├── src ├── commands │ ├── init │ │ ├── library.ts │ │ ├── types.ts │ │ ├── help.ts │ │ ├── repositories.ts │ │ ├── scene.ts │ │ ├── utils.ts │ │ └── index.ts │ ├── index.ts │ ├── export.ts │ ├── install.ts │ ├── status.ts │ ├── workspace.ts │ ├── pack.ts │ ├── coords.ts │ ├── build.ts │ └── info.ts ├── utils │ ├── pathsExistOnDir.ts │ ├── getDummyMappings.ts │ ├── env.ts │ ├── errors.ts │ ├── buildProject.ts │ ├── index.ts │ ├── land.ts │ ├── getProjectFilePaths.ts │ ├── spinner.ts │ ├── download.ts │ ├── project.ts │ ├── installedDependencies.ts │ ├── catalystPointers.ts │ ├── nodeAndNpmVersion.ts │ ├── filesystem.ts │ ├── shellCommands.ts │ ├── logging.ts │ ├── coordinateHelpers.ts │ ├── moduleHelpers.ts │ └── analytics.ts ├── project │ ├── isTypescriptProject.ts │ ├── installDependencies.ts │ ├── updateBundleDependenciesField.ts │ └── projectInfo.ts ├── lib │ ├── content │ │ ├── types.ts │ │ └── ContentService.ts │ ├── smartItems │ │ ├── buildSmartItem.ts │ │ └── packProject.ts │ ├── adapters │ │ ├── ws.ts │ │ └── proto │ │ │ ├── comms.proto │ │ │ ├── broker.proto │ │ │ └── comms.d.ts │ ├── IEthereumDataProvider.ts │ ├── controllers │ │ ├── debugger.ts │ │ ├── bff.ts │ │ └── legacy-comms-v1.ts │ ├── WorldsContentServer.ts │ ├── WorldsContentServerLinkerAPI.ts │ └── Preview.ts ├── index.ts ├── sceneJson │ ├── index.ts │ ├── lintSceneFile.ts │ └── utils.ts ├── config.ts └── main.ts ├── .eslintignore ├── .gitattributes ├── test ├── unit │ ├── resources │ │ └── data │ │ │ ├── assets │ │ │ └── test.txt │ │ │ └── scene.json │ ├── utils │ │ ├── pathsExistOnDir.test.ts │ │ ├── getProjectFilePaths.test.ts │ │ ├── getDummyMappings.test.ts │ │ └── gettingPackageJsonAndVersioning.test.ts │ ├── lib │ │ └── Ethereum.test.ts │ └── sceneJson │ │ └── utils.test.ts ├── fixtures │ └── ecs-compiled │ │ ├── src │ │ ├── utils │ │ │ └── index.js │ │ └── game.ts │ │ ├── .dclignore │ │ ├── tsconfig.json │ │ ├── Dockerfile │ │ ├── package.json │ │ ├── scene.json │ │ └── package-lock.json ├── e2e │ ├── snapshots │ │ ├── dcl-preview.png │ │ ├── build.test.ts.snap │ │ ├── coords.test.ts.snap │ │ ├── deploy.test.ts.snap │ │ ├── export.test.ts.snap │ │ ├── help.test.ts.snap │ │ ├── info.test.ts.snap │ │ ├── init.test.ts.snap │ │ ├── start.test.ts.snap │ │ ├── status.test.ts.snap │ │ ├── install.test.ts.snap │ │ ├── workspace.test.ts.snap │ │ ├── world-acl.test.ts.snap │ │ ├── dcl-preview-modified.1.png │ │ ├── dcl-preview-modified.2.png │ │ ├── export.test.ts.md │ │ ├── install.test.ts.md │ │ ├── workspace.test.ts.md │ │ ├── build.test.ts.md │ │ ├── status.test.ts.md │ │ ├── init.test.ts.md │ │ ├── coords.test.ts.md │ │ ├── info.test.ts.md │ │ ├── deploy.test.ts.md │ │ ├── world-acl.test.ts.md │ │ └── help.test.ts.md │ ├── workspace.test.ts │ ├── export.test.ts │ ├── world-acl.test.ts │ ├── version.test.ts │ ├── info.test.ts │ ├── status.test.ts │ ├── build.test.ts │ ├── help.test.ts │ ├── install.test.ts │ ├── init.test.ts │ ├── coords.test.ts │ ├── start.test.ts │ └── index.test.ts ├── tsconfig.json ├── helpers │ ├── buildProject.ts │ ├── initProject.ts │ ├── sandbox.ts │ └── commando.ts └── integration │ ├── helpers │ └── index.ts │ └── Project.test.ts ├── .dockerignore ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md └── workflows │ └── ci.yml ├── Dockerfile ├── typings └── dcl.d.ts ├── .eslintrc.json ├── samples └── workspace │ ├── package.json │ └── Dockerfile ├── tsconfig.json ├── certs ├── localhost.crt └── localhost.key ├── README.md └── package.json /src/commands/init/library.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | samples/ 2 | dist/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /test/unit/resources/data/assets/test.txt: -------------------------------------------------------------------------------- 1 | something 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | tmp 4 | *.log 5 | dist 6 | .DS_Store 7 | test-* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | tmp 4 | *.log 5 | dist 6 | .DS_Store 7 | test-* 8 | decentraland-*.tgz -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function utilFunction() { 2 | return {} 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/snapshots/dcl-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/dcl-preview.png -------------------------------------------------------------------------------- /test/e2e/snapshots/build.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/build.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/coords.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/coords.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/deploy.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/deploy.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/export.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/export.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/help.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/help.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/info.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/info.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/init.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/init.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/start.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/start.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/status.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/status.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/install.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/install.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/workspace.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/workspace.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/world-acl.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/world-acl.test.ts.snap -------------------------------------------------------------------------------- /test/e2e/snapshots/dcl-preview-modified.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/dcl-preview-modified.1.png -------------------------------------------------------------------------------- /test/e2e/snapshots/dcl-preview-modified.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentraland/cli/HEAD/test/e2e/snapshots/dcl-preview-modified.2.png -------------------------------------------------------------------------------- /test/e2e/workspace.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { help } from '../../src/commands/workspace' 4 | 5 | test('snapshot - dcl help instal', (t) => { 6 | t.snapshot(help()) 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/export.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as exportCmd from '../../src/commands/export' 3 | 4 | test('snapshot - dcl help export', (t) => { 5 | t.snapshot(exportCmd.help()) 6 | }) 7 | -------------------------------------------------------------------------------- /test/e2e/world-acl.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as worldAcl from '../../src/commands/world-acl' 3 | 4 | test('snapshot - dcl help world-acl', (t) => { 5 | t.snapshot(worldAcl.help()) 6 | }) 7 | -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/.dclignore: -------------------------------------------------------------------------------- 1 | .* 2 | package.json 3 | package-lock.json 4 | yarn-lock.json 5 | build.json 6 | export 7 | tsconfig.json 8 | tslint.json 9 | node_modules 10 | *.ts 11 | *.tsx 12 | Dockerfile 13 | dist/ -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outFile": "./bin/game.js", 4 | "allowJs": true 5 | }, 6 | "include": ["src/**/*.ts"], 7 | "extends": "./node_modules/decentraland-ecs/types/tsconfig.json" 8 | } 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # What? 7 | 8 | ... 9 | 10 | # Why? 11 | 12 | ... 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /usr/src/app 3 | 4 | COPY package-lock.json . 5 | COPY package.json . 6 | 7 | RUN npm install 8 | COPY . . 9 | RUN npm link 10 | RUN npm run build 11 | 12 | EXPOSE 8080 13 | 14 | ENTRYPOINT [ "/bin/bash" ] -------------------------------------------------------------------------------- /src/utils/pathsExistOnDir.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | export default function pathsExistOnDir(dir: string, filepaths: string[]): Promise { 5 | return Promise.all(filepaths.map((f) => fs.pathExists(path.resolve(dir, f)))) 6 | } 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | allow: 8 | - dependency-name: "@dcl/*" 9 | - dependency-name: "dcl-*" 10 | versioning-strategy: increase 11 | -------------------------------------------------------------------------------- /src/project/isTypescriptProject.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | export async function isTypescriptProject(projectPath: string): Promise { 5 | const tsconfigPath = path.resolve(projectPath, 'tsconfig.json') 6 | return fs.pathExists(tsconfigPath) 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export default new Set([ 2 | 'init', 3 | 'build', 4 | 'start', 5 | 'deploy', 6 | 'deploy-deprecated', 7 | 'info', 8 | 'status', 9 | 'help', 10 | 'export', 11 | 'pack', 12 | 'install', 13 | 'coords', 14 | 'world-acl', 15 | 'workspace' 16 | ]) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # What? 8 | 9 | ... 10 | 11 | # Why? 12 | 13 | ... 14 | -------------------------------------------------------------------------------- /src/lib/content/types.ts: -------------------------------------------------------------------------------- 1 | export type ContentPair = { 2 | file: string 3 | hash: string 4 | } 5 | 6 | export type MappingsFile = { 7 | parcel_id: string 8 | publisher: string 9 | root_cid: string 10 | contents: ContentPair[] 11 | // This mappings field is a backwards compatibility field. 12 | mappings: Record 13 | } 14 | -------------------------------------------------------------------------------- /typings/dcl.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | export const abi: any 3 | } 4 | 5 | declare module 'wildcards' { 6 | const wildcards: (...args: unknown) => void 7 | export = wildcards 8 | export default wildcards 9 | } 10 | 11 | declare module 'opn' { 12 | const opn: (...args: unknown) => Promise 13 | export = opn 14 | export default opn 15 | } 16 | -------------------------------------------------------------------------------- /test/e2e/snapshots/export.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/export.test.ts` 2 | 3 | The actual snapshot is saved in `export.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help export 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | dcl export was deprecated in 3.10.0 version of the Decentraland CLI.␊ 13 | ` 14 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "types": [ 9 | "node", 10 | ], 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "resolveJsonModule": true 16 | } 17 | } -------------------------------------------------------------------------------- /test/helpers/buildProject.ts: -------------------------------------------------------------------------------- 1 | import Commando from '../helpers/commando' 2 | import { isDebug } from '../../src/utils/env' 3 | 4 | export default function buildProject(dirPath) { 5 | return new Promise((resolve) => { 6 | new Commando(`npm run build`, { 7 | silent: !isDebug(), 8 | workingDir: dirPath, 9 | env: { NODE_ENV: 'development' } 10 | }).on('end', async () => { 11 | resolve() 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/utils/pathsExistOnDir.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import path from 'path' 3 | 4 | import pathsExistOnDir from '../../../src/utils/pathsExistOnDir' 5 | 6 | test('Unit - pathsExistOnDir() - should return all boolean existances of files on dir', async (t) => { 7 | const dir = path.join(__dirname, '../../fixtures/ecs-compiled') 8 | 9 | const filesExist = await pathsExistOnDir(dir, ['scene.json', 'bin/game.js']) 10 | t.deepEqual(filesExist, [true, true]) 11 | }) 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@dcl/eslint-config/sdk", 3 | "parserOptions": { 4 | "project": [ 5 | "tsconfig.json", 6 | "test/tsconfig.json" 7 | ] 8 | }, 9 | "rules": { 10 | "eqeqeq": ["error", "always"], 11 | "@typescript-eslint/no-non-null-assertion": "off", 12 | "@typescript-eslint/no-unused-vars": [ 13 | "warn", 14 | { 15 | "ignoreRestSiblings": true, 16 | "argsIgnorePattern": "^_" 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import { main } from './main' 7 | 8 | fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8', (err, data) => { 9 | if (err) { 10 | console.error('There was an unexpected error.', err) 11 | process.exit(1) 12 | } 13 | 14 | const { version } = JSON.parse(data) 15 | main(version).catch((err: any) => { 16 | console.error(err) 17 | process.exit(1) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/lib/smartItems/buildSmartItem.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | import buildProject from '../../utils/buildProject' 5 | 6 | export async function buildSmartItem(workingDir: string): Promise { 7 | const gamePath = path.resolve(workingDir, 'src', 'game.ts') 8 | const gameFile = await fs.readFile(gamePath, 'utf-8') 9 | await fs.writeFile(gamePath, gameFile.replace(/\n/g, '\n//'), 'utf-8') 10 | await buildProject(workingDir) 11 | return fs.writeFile(gamePath, gameFile, 'utf-8') 12 | } 13 | -------------------------------------------------------------------------------- /samples/workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcl-project", 3 | "version": "1.0.0", 4 | "description": "My new Decentraland project", 5 | "scripts": { 6 | "start": "dcl start", 7 | "build": "build-ecs", 8 | "watch": "build-ecs --watch", 9 | "deploy:now": "dcl export && now export", 10 | "ecs:install": "npm install --save-dev decentraland-ecs@latest", 11 | "ecs:install-next": "npm install --save-dev decentraland-ecs@next" 12 | }, 13 | "devDependencies": { 14 | "decentraland-ecs": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/integration/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | export async function setupFilesystem(dirPath: string, files: { path: string; content: string }[]) { 5 | for (let i = 0; i < files.length; i++) { 6 | const file = files[i] 7 | const filePath = path.resolve(dirPath, file.path) 8 | const fileDir = path.dirname(filePath) 9 | 10 | if (fileDir !== dirPath) { 11 | await fs.mkdirp(fileDir) 12 | } 13 | 14 | await fs.writeFile(filePath, file.content) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/init/types.ts: -------------------------------------------------------------------------------- 1 | import { sdk } from '@dcl/schemas' 2 | 3 | type URL = string 4 | 5 | export type InitOptionProjectType = { 6 | type: 'project' 7 | value: sdk.ProjectType 8 | } 9 | 10 | export type InitOptionRepositoryURL = { 11 | type: 'scene' 12 | value: URL 13 | } 14 | 15 | export type InitOption = InitOptionProjectType | InitOptionRepositoryURL 16 | 17 | export type RepositoryJson = { 18 | scenes: { title: string; url: string }[] 19 | library: string 20 | portableExperience: string 21 | smartItem: string 22 | } 23 | -------------------------------------------------------------------------------- /samples/workspace/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | RUN npm install --global decentraland@next 7 | 8 | # Install app dependencies 9 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 10 | # where available (npm@5+) 11 | COPY package*.json ./ 12 | 13 | RUN npm install 14 | # If you are building your code for production 15 | # RUN npm install --only=production 16 | 17 | # Bundle app source 18 | COPY . . 19 | 20 | EXPOSE 8000 21 | CMD [ "npm", "start", "--", "--ci" ] -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | RUN npm install --global decentraland@next 7 | 8 | # Install app dependencies 9 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 10 | # where available (npm@5+) 11 | COPY package*.json ./ 12 | 13 | RUN npm install 14 | # If you are building your code for production 15 | # RUN npm install --only=production 16 | 17 | # Bundle app source 18 | COPY . . 19 | 20 | EXPOSE 8000 21 | CMD [ "npm", "start", "--", "--ci" ] -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcl-project", 3 | "version": "1.0.0", 4 | "description": "My new Decentraland project", 5 | "scripts": { 6 | "start": "dcl start", 7 | "build": "build-ecs", 8 | "watch": "build-ecs --watch", 9 | "deploy:now": "now --platform-version 1 --docker", 10 | "ecs:install": "npm install --save-dev decentraland-ecs@latest", 11 | "ecs:install-next": "npm install --save-dev decentraland-ecs@next" 12 | }, 13 | "devDependencies": { 14 | "decentraland-ecs": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/export.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Analytics } from '../utils/analytics' 3 | 4 | export const help = () => ` 5 | ${chalk.bold('dcl export')} was deprecated in 3.10.0 version of the Decentraland CLI. 6 | ` 7 | export async function main(): Promise { 8 | const link = 'https://docs.decentraland.org/development-guide/deploy-to-now/' 9 | console.warn(`\`dcl export\` is not being supported in this CLI version. Please visit ${link} to more information`) 10 | Analytics.tryToUseDeprecated({ command: 'export' }) 11 | return 1 12 | } 13 | -------------------------------------------------------------------------------- /src/sceneJson/index.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from '@dcl/schemas' 2 | import path from 'path' 3 | import fs from 'fs-extra' 4 | 5 | let sceneFile: Scene 6 | 7 | export async function getSceneFile(workingDir: string, cache: boolean = true): Promise { 8 | if (cache && sceneFile) { 9 | return sceneFile 10 | } 11 | 12 | sceneFile = await fs.readJSON(path.resolve(workingDir, 'scene.json')) 13 | return sceneFile 14 | } 15 | 16 | export async function setSceneFile(sceneFile: Scene, workingDir: string): Promise { 17 | return fs.writeJSON(path.resolve(workingDir, 'scene.json'), sceneFile, { 18 | spaces: 2 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/adapters/ws.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws' 2 | import { PreviewComponents } from '../Preview' 3 | import { IBaseComponent } from '@well-known-components/interfaces' 4 | 5 | export type WebSocketComponent = IBaseComponent & { 6 | ws: WebSocketServer 7 | } 8 | 9 | /** 10 | * Creates a http-server component 11 | * @public 12 | */ 13 | export async function createWsComponent(_: Pick): Promise { 14 | const ws = new WebSocketServer({ noServer: true }) 15 | 16 | async function stop() { 17 | ws.close() 18 | } 19 | 20 | return { 21 | stop, 22 | ws 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "display": { 3 | "title": "interactive-text", 4 | "favicon": "favicon_asset" 5 | }, 6 | "owner": "", 7 | "contact": { 8 | "name": "author-name", 9 | "email": "" 10 | }, 11 | "main": "bin/game.js", 12 | "tags": [], 13 | "scene": { 14 | "parcels": ["0,0"], 15 | "base": "0,0" 16 | }, 17 | "communications": { 18 | "type": "webrtc", 19 | "signalling": "https://signalling-01.decentraland.org" 20 | }, 21 | "policy": { 22 | "contentRating": "E", 23 | "fly": true, 24 | "voiceEnabled": true, 25 | "blacklist": [], 26 | "teleportPosition": "" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/unit/utils/getProjectFilePaths.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import path from 'path' 3 | import fs from 'fs-extra' 4 | 5 | import getProjectFilePaths from '../../../src/utils/getProjectFilePaths' 6 | 7 | test('Unit - getProjectFilePaths() - should return all filtered project files', async (t) => { 8 | const dir = path.join(__dirname, '../../fixtures/ecs-compiled') 9 | const dclIgnoreContent = await fs.readFile(path.join(__dirname, '../../fixtures/ecs-compiled/.dclignore'), 'utf-8') 10 | 11 | const filePaths = await getProjectFilePaths(dir, dclIgnoreContent) 12 | t.deepEqual(filePaths, ['scene.json', 'bin/game.js', 'src/utils/index.js']) 13 | }) 14 | -------------------------------------------------------------------------------- /src/sceneJson/lintSceneFile.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from '@dcl/schemas' 2 | 3 | import { getSceneFile, setSceneFile } from '.' 4 | 5 | export async function lintSceneFile(workingDir: string): Promise { 6 | const sceneFile = await getSceneFile(workingDir) 7 | const finalScene: Scene = { 8 | ...sceneFile, 9 | scene: { 10 | ...sceneFile.scene, 11 | base: sceneFile.scene.base.replace(/\ /g, ''), 12 | parcels: sceneFile.scene.parcels.map((coords) => coords.replace(/\ /g, '')) 13 | } 14 | } 15 | 16 | if (JSON.stringify(sceneFile) !== JSON.stringify(finalScene)) { 17 | return setSceneFile(finalScene, workingDir) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/getDummyMappings.ts: -------------------------------------------------------------------------------- 1 | import { MappingsFile } from '../lib/content/types' 2 | 3 | export default function getDummyMappings(filePaths: string[]): MappingsFile { 4 | // In case of Windows 5 | const finalPaths = filePaths.map((f) => f.replace(/\\/g, '/')) 6 | 7 | const mappings = finalPaths.reduce((acc: Record, f) => { 8 | acc[f] = f 9 | return acc 10 | }, {}) 11 | 12 | return { 13 | mappings, 14 | contents: Object.entries(mappings).map(([file, hash]) => ({ file, hash })), 15 | parcel_id: '0,0', 16 | publisher: '0x0000000000000000000000000000000000000000', 17 | root_cid: 'Qm0000000000000000000000000000000000000000' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { getOrElse } from '.' 2 | 3 | export function isDevelopment() { 4 | if (!process.env.NODE_ENV) { 5 | try { 6 | require.resolve('decentraland-eth') 7 | return true 8 | } catch (e) { 9 | return false 10 | } 11 | } 12 | 13 | return process.env.NODE_ENV !== 'production' 14 | } 15 | 16 | export function isDebug() { 17 | return !!process.env.DEBUG 18 | } 19 | 20 | export const isDev: boolean = process.env.DCL_ENV === 'dev' 21 | export function getProvider() { 22 | return isDev ? 'https://rpc.decentraland.org/sepolia' : 'https://rpc.decentraland.org/mainnet' 23 | } 24 | 25 | export function isEnvCi(): boolean { 26 | return getOrElse(process.env.CI, false) 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorType { 2 | INIT_ERROR = 'InitError', 3 | LINKER_ERROR = 'LinkerError', 4 | ETHEREUM_ERROR = 'EthereumError', 5 | PROJECT_ERROR = 'ProjectError', 6 | PREVIEW_ERROR = 'PreviewError', 7 | UPGRADE_ERROR = 'UpgradeError', 8 | INFO_ERROR = 'InfoError', 9 | STATUS_ERROR = 'StatusError', 10 | DEPLOY_ERROR = 'DeployError', 11 | API_ERROR = 'APIError', 12 | UPLOAD_ERROR = 'UploadError', 13 | CONTENT_SERVER_ERROR = 'ContentServerError', 14 | WORLD_CONTENT_SERVER_ERROR = 'WorldContentServerError', 15 | WORKSPACE_ERROR = 'WorkspaceError' 16 | } 17 | 18 | export function fail(type: ErrorType, message: string) { 19 | const e = new Error(message) 20 | e.name = type 21 | throw e 22 | } 23 | -------------------------------------------------------------------------------- /test/e2e/snapshots/install.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/install.test.ts` 2 | 3 | The actual snapshot is saved in `install.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help instal 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl install [package]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | ␊ 18 | Examples:␊ 19 | ␊ 20 | - Install a new package␊ 21 | ␊ 22 | $ dcl install package-example␊ 23 | ␊ 24 | - Check the Decentraland libraries used are in bundleDependencies␊ 25 | ␊ 26 | $ dcl install␊ 27 | ` 28 | -------------------------------------------------------------------------------- /test/helpers/initProject.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import Commando, { Response } from '../helpers/commando' 4 | import { isDebug } from '../../src/utils/env' 5 | 6 | export default function initProject(dirPath, installDep = true) { 7 | return new Promise((resolve) => { 8 | const cmd = new Commando(`node ${path.resolve('dist', 'index.js')} init -p scene`, { 9 | silent: !isDebug(), 10 | workingDir: dirPath, 11 | env: { NODE_ENV: 'development' } 12 | }).when(/Send anonymous usage stats to Decentraland?/, () => Response.NO) 13 | 14 | if (!installDep) { 15 | cmd.endWhen(/Installing dependencies/) 16 | } 17 | 18 | cmd.on('end', async () => { 19 | resolve() 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /test/e2e/version.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | 4 | import { isDebug } from '../../src/utils/env' 5 | import Commando from '../helpers/commando' 6 | import { version } from '../../package.json' 7 | 8 | test('version command', async (t) => { 9 | const dclVersion: string = await new Promise((resolve) => { 10 | let allData = '' 11 | new Commando( 12 | `node ${path.resolve('dist', 'index.js')} version`, 13 | { 14 | silent: !isDebug(), 15 | workingDir: '.', 16 | env: { NODE_ENV: 'development' } 17 | }, 18 | (data) => (allData += data) 19 | ).on('end', async () => { 20 | resolve(allData) 21 | }) 22 | }) 23 | 24 | t.true(dclVersion.includes(version)) 25 | }) 26 | -------------------------------------------------------------------------------- /test/e2e/snapshots/workspace.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/workspace.test.ts` 2 | 3 | The actual snapshot is saved in `workspace.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help instal 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl workspace SUBCOMMAND [options]␊ 13 | ␊ 14 | Sub commands:␊ 15 | ␊ 16 | init Create a workspace looking for subfolder Decentraland projects.␊ 17 | ls List all projects in the current workspace␊ 18 | add Add a project in the current workspace.␊ 19 | ␊ 20 | Options:␊ 21 | ␊ 22 | -h, --help Displays complete help␊ 23 | ` 24 | -------------------------------------------------------------------------------- /src/lib/smartItems/packProject.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import archiver from 'archiver' 3 | 4 | export async function packProject(files: string[], target: string) { 5 | const output = fs.createWriteStream(target) 6 | const archive = archiver('zip') 7 | 8 | return new Promise((resolve, reject) => { 9 | output.on('close', () => { 10 | resolve() 11 | }) 12 | 13 | archive.on('warning', (err) => { 14 | reject(err) 15 | }) 16 | 17 | archive.on('error', (err) => { 18 | reject(err) 19 | }) 20 | 21 | archive.pipe(output) 22 | 23 | const targetFiles = files.filter((f) => f !== '') 24 | targetFiles.forEach((f) => { 25 | archive.file(f, { name: f }) 26 | }) 27 | 28 | return archive.finalize() 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/buildProject.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | 3 | const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm' 4 | 5 | export default function buildProject(workingDir: string): Promise { 6 | console.log(`Building project using "npm run build"`) 7 | return new Promise((resolve, reject) => { 8 | const child = spawn(npm, ['run', 'build'], { 9 | shell: true, 10 | cwd: workingDir, 11 | env: { ...process.env, NODE_ENV: '' } 12 | }) 13 | 14 | child.stdout.pipe(process.stdout) 15 | child.stderr.pipe(process.stderr) 16 | 17 | child.on('close', (code) => { 18 | if (code !== 0) { 19 | reject(new Error('Error while building the project')) 20 | } else { 21 | resolve() 22 | } 23 | }) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "preserveConstEnums": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "diagnostics": true, 11 | "sourceMap": true, 12 | "lib": [ 13 | "es2016" 14 | ], 15 | "types": [ 16 | "node" 17 | ], 18 | "strict": true, 19 | "outDir": "./dist", 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "allowJs": true 24 | }, 25 | "exclude": [ 26 | "dist", 27 | "node_modules", 28 | "**/*.test.ts", 29 | "samples/*" 30 | ], 31 | "include": [ 32 | "decs.d.ts", 33 | "src", 34 | "typings" 35 | ] 36 | } -------------------------------------------------------------------------------- /test/e2e/info.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | 4 | import * as info from '../../src/commands/info' 5 | import { isDebug } from '../../src/utils/env' 6 | import Commando from '../helpers/commando' 7 | 8 | test('snapshot - dcl help info', (t) => { 9 | t.snapshot(info.help()) 10 | }) 11 | 12 | test('E2E - info command', async (t) => { 13 | await new Promise((resolve) => { 14 | let allData = '' 15 | new Commando( 16 | `node ${path.resolve('dist', 'index.js')} info --network sepolia -35,-130`, 17 | { 18 | silent: !isDebug(), 19 | workingDir: '.', 20 | env: { NODE_ENV: 'development' } 21 | }, 22 | (data) => (allData += data) 23 | ).on('end', async () => { 24 | t.snapshot(allData) 25 | resolve() 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/e2e/status.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | 4 | import * as status from '../../src/commands/status' 5 | import { isDebug } from '../../src/utils/env' 6 | import Commando from '../helpers/commando' 7 | 8 | test('snapshot - dcl help status', (t) => { 9 | t.snapshot(status.help()) 10 | }) 11 | 12 | test('E2E - status command', async (t) => { 13 | await new Promise((resolve) => { 14 | let allData = '' 15 | new Commando( 16 | `node ${path.resolve('dist', 'index.js')} status --network sepolia -35,-130`, 17 | { 18 | silent: !isDebug(), 19 | workingDir: '.', 20 | env: { NODE_ENV: 'development' } 21 | }, 22 | (data) => (allData += data) 23 | ).on('end', async () => { 24 | t.snapshot(allData) 25 | resolve() 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/unit/resources/data/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "display": { 3 | "title": "DCL Scene", 4 | "description": "My new Decentraland project", 5 | "navmapThumbnail": "images/scene-thumbnail.png", 6 | "favicon": "favicon_asset" 7 | }, 8 | "owner": "", 9 | "contact": { 10 | "name": "author-name", 11 | "email": "" 12 | }, 13 | "main": "bin/game.js", 14 | "tags": [], 15 | "scene": { 16 | "parcels": ["0,0"], 17 | "base": "0,0" 18 | }, 19 | "spawnPoints": [ 20 | { 21 | "name": "spawn1", 22 | "default": true, 23 | "position": { 24 | "x": 0, 25 | "y": 0, 26 | "z": 0 27 | }, 28 | "cameraTarget": { 29 | "x": 8, 30 | "y": 1, 31 | "z": 8 32 | } 33 | } 34 | ], 35 | "requiredPermissions": [], 36 | "featureToggles": {} 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function getOrElse(value: any, def: any) { 2 | return value !== undefined ? value : def 3 | } 4 | 5 | /** 6 | * Returns an object with the specified attributes with null as value 7 | */ 8 | export function createEmptyObj(attributes: string[], obj: any = {}) { 9 | attributes.forEach((attr) => { 10 | obj[attr] = null 11 | }) 12 | return obj 13 | } 14 | 15 | /** 16 | * Filter undefined keys from provided object 17 | */ 18 | export function removeEmptyKeys(obj: Record) { 19 | const result: Record = {} 20 | Object.keys(obj) 21 | .filter((k) => !!obj[k]) 22 | .forEach((k) => (result[k] = obj[k])) 23 | return result 24 | } 25 | 26 | export function isRecord(obj: unknown): obj is Record { 27 | return typeof obj === 'object' && !Array.isArray(obj) && !!obj 28 | } 29 | -------------------------------------------------------------------------------- /test/unit/utils/getDummyMappings.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import getDummyMappings from '../../../src/utils/getDummyMappings' 4 | 5 | test('Unit - getDummyMappings() - should calculate mappings for outside of DCL', async (t) => { 6 | const expected = { 7 | mappings: { 8 | 'bin/game.js': 'bin/game.js', 9 | 'scene.json': 'scene.json' 10 | }, 11 | contents: [ 12 | { 13 | file: 'scene.json', 14 | hash: 'scene.json' 15 | }, 16 | { 17 | file: 'bin/game.js', 18 | hash: 'bin/game.js' 19 | } 20 | ], 21 | parcel_id: '0,0', 22 | publisher: '0x0000000000000000000000000000000000000000', 23 | root_cid: 'Qm0000000000000000000000000000000000000000' 24 | } 25 | 26 | const mappings = await getDummyMappings(['scene.json', 'bin/game.js']) 27 | t.deepEqual(mappings, expected) 28 | }) 29 | -------------------------------------------------------------------------------- /src/lib/IEthereumDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { LANDData } from './Ethereum' 2 | import { Coords } from '../utils/coordinateHelpers' 3 | 4 | export interface IEthereumDataProvider { 5 | getEstateIdOfLand: (coords: Coords) => Promise 6 | getEstateData: (estateId: number) => Promise 7 | getEstateOwner: (estateId: number) => Promise 8 | getEstateOperator: (estateId: number) => Promise 9 | getEstateUpdateOperator: (estateId: number) => Promise 10 | getLandOfEstate: (estateId: number) => Promise 11 | getLandData: (coords: Coords) => Promise 12 | getLandOwner: (coords: Coords) => Promise 13 | getLandOperator: (coords: Coords) => Promise 14 | getLandUpdateOperator: (coords: Coords) => Promise 15 | getLandOf: (owner: string) => Promise 16 | getEstatesOf: (owner: string) => Promise 17 | } 18 | -------------------------------------------------------------------------------- /test/e2e/snapshots/build.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/build.test.ts` 2 | 3 | The actual snapshot is saved in `build.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help build 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl build [options]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | -w, --watch Watch for file changes and build on change␊ 18 | -p, --production Build without sourcemaps␊ 19 | --skip-version-checks Skip the ECS and CLI version checks, avoid the warning message and launch anyway␊ 20 | --skip-install Skip installing dependencies␊ 21 | ␊ 22 | Example:␊ 23 | ␊ 24 | - Build your scene:␊ 25 | ␊ 26 | $ dcl build␊ 27 | ` 28 | -------------------------------------------------------------------------------- /test/unit/lib/Ethereum.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { Ethereum } from '../../../src/lib/Ethereum' 4 | 5 | test('Unit - Ethereum.decodeLandData()', (t) => { 6 | const eth = new Ethereum() 7 | 8 | t.deepEqual(eth['decodeLandData']('0,"myLand","my description","QmYeRMVLAtHCzGUbFSBbTTSUYx4AnqHZWwXAy5jzVJSpCE"'), { 9 | version: 0, 10 | name: 'myLand', 11 | description: 'my description' 12 | }) 13 | 14 | t.deepEqual(eth['decodeLandData']('0,"myLand","my description",'), { 15 | version: 0, 16 | name: 'myLand', 17 | description: 'my description' 18 | }) 19 | 20 | t.deepEqual(eth['decodeLandData']('0,,,'), { 21 | version: 0, 22 | name: null, 23 | description: null 24 | }) 25 | 26 | t.deepEqual(eth['decodeLandData']('0,"",,"QmYeRMVLAtHCzGUbFSBbTTSUYx4AnqHZWwXAy5jzVJSpCE"'), { 27 | version: 0, 28 | name: null, 29 | description: null 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/e2e/snapshots/status.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/status.test.ts` 2 | 3 | The actual snapshot is saved in `status.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help status 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl status [target] [options]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | -n, --network Choose between mainnet and sepolia (default 'mainnet')␊ 18 | ␊ 19 | ␊ 20 | Examples:␊ 21 | ␊ 22 | - Get Decentraland Scene information of the current project"␊ 23 | ␊ 24 | $ dcl status␊ 25 | ␊ 26 | - Get Decentraland Scene information of the parcel -12, 40"␊ 27 | ␊ 28 | $ dcl status -12,40␊ 29 | ` 30 | 31 | ## E2E - status command 32 | 33 | > Snapshot 1 34 | 35 | '' 36 | -------------------------------------------------------------------------------- /src/utils/land.ts: -------------------------------------------------------------------------------- 1 | import { LANDData } from '../lib/Ethereum' 2 | 3 | export function filterAndFillEmpty(data: any, def?: string): LANDData { 4 | if (!data) { 5 | return { name: def || '', description: def || '' } 6 | } 7 | 8 | return { name: data.name || def, description: data.description || def } 9 | } 10 | 11 | export function parseTarget(args: any) { 12 | const args1 = parseInt(args[1], 10) 13 | if (Number.isInteger(args1) && args1 < 0) { 14 | let coords = '-' 15 | for (let i = 0; i < args.length; i++) { 16 | if (args[i] === '-,') { 17 | coords += ',' 18 | continue 19 | } 20 | 21 | const uint = args[i].substring(1) 22 | if (!Number.isInteger(parseInt(uint, 10))) { 23 | continue 24 | } 25 | 26 | if (args[i - 1] === '--') { 27 | coords += `-${uint}` 28 | continue 29 | } 30 | 31 | coords += uint 32 | } 33 | return coords 34 | } 35 | 36 | return args[1] 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/adapters/proto/comms.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | 5 | message AuthData { 6 | string signature = 1; 7 | string identity = 2; 8 | string timestamp = 3; 9 | string access_token = 4; 10 | } 11 | 12 | enum Category { 13 | UNKNOWN = 0; 14 | POSITION = 1; 15 | PROFILE = 2; 16 | CHAT = 3; 17 | SCENE_MESSAGE = 4; 18 | } 19 | 20 | message DataHeader { 21 | Category category = 1; 22 | } 23 | 24 | message PositionData { 25 | Category category = 1; 26 | double time = 2; 27 | float position_x = 3; 28 | float position_y = 4; 29 | float position_z = 5; 30 | float rotation_x = 6; 31 | float rotation_y = 7; 32 | float rotation_z = 8; 33 | float rotation_w = 9; 34 | } 35 | 36 | message ProfileData { 37 | Category category = 1; 38 | double time = 2; 39 | string profile_version = 3; 40 | } 41 | 42 | message ChatData { 43 | Category category = 1; 44 | double time = 2; 45 | string message_id = 3; 46 | string text = 4; 47 | } -------------------------------------------------------------------------------- /src/project/installDependencies.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import chalk from 'chalk' 3 | 4 | import * as spinner from '../utils/spinner' 5 | import { npm } from '../utils/moduleHelpers' 6 | 7 | export default function installDependencies(workingDir: string, silent: boolean): Promise { 8 | spinner.create('Installing dependencies') 9 | return new Promise((resolve, reject) => { 10 | const child = spawn(npm, ['install'], { 11 | shell: true, 12 | cwd: workingDir, 13 | env: { ...process.env, NODE_ENV: '' } 14 | }) 15 | 16 | if (!silent) { 17 | child.stdout.pipe(process.stdout) 18 | child.stderr.pipe(process.stderr) 19 | } 20 | 21 | child.on('close', (code) => { 22 | if (code !== 0) { 23 | spinner.fail() 24 | reject( 25 | new Error(`${chalk.bold(`npm install`)} exited with code ${code}. Please try running the command manually`) 26 | ) 27 | return 28 | } 29 | 30 | spinner.succeed('Dependencies installed.') 31 | resolve() 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /test/e2e/snapshots/init.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/init.test.ts` 2 | 3 | The actual snapshot is saved in `init.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help init 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl init [options]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | -p, --project [type] Choose a projectType (default is scene). It could be any of scene, smart-item, portable-experience, library␊ 18 | ␊ 19 | Examples:␊ 20 | ␊ 21 | - Generate a new Decentraland Scene project in my-project folder␊ 22 | ␊ 23 | $ dcl init my-project␊ 24 | ␊ 25 | - Generate a new scene project␊ 26 | ␊ 27 | $ dcl init --project scene␊ 28 | ␊ 29 | --skip-install Skip installing dependencies␊ 30 | --template The URL to a template. It must be under the decentraland or decentraland-scenes GitHub organization.␊ 31 | ` 32 | -------------------------------------------------------------------------------- /certs/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5TCCAc2gAwIBAgIJANUDQEa553dtMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xODA4MDkxODM1NTRaFw0xODA5MDgxODM1NTRaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBALgvCvCaIJ310JUh2z36kAHs/35VN9bl4RyTg6rKI9DG+MkU/iOu978VzGtH 6 | CvWeczrh8AtGtxVJGOmSH1S/4BXZnRByEiGnmd38QuONHFsHnua0MGFS9GohG0ed 7 | kfInvd518Chd0TDCn/+nMIFPPE6SzlhQPI0DwrY56Bns9MbkoMYtJ5rJQJ1Vv/iE 8 | div6wkmtxEwjxAyYdJckqiozDlfvxH3cAXBBPP1UYgwlqLgjvvlyV4oYM3/jxh2B 9 | Jd0U4xs/RxyZRqD90gbj6n1upv4RD3sOHN/OrlJIa2ZjWAYAAZaMBjrKXqhQsuB2 10 | CQy/bMlMRLrUAaiblBpgKncAKpcCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo 11 | b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B 12 | AQsFAAOCAQEAMo/zgCEq9wBSkS3P0OaFOQyg8EVxNPaJW3kHFsr6DDLvUcRTKw9d 13 | 5x+8JC3puIKRLrK/Uz+6OyYcT9Iit4DYHsvN9JnhemPLo3I0J3BK/0jJqhJhWN6F 14 | m30+jWfJ8QrPr9+qj3P0iNW+0idDtwSjedj2Hk4aL4II2j1aWeUVZBGzq/uIs7bz 15 | VuCIsxKboGf3L4F2ruCA+nB/pnASn9d9bMkwdX5DwQxFhBziDZm79ASuRbggi8xC 16 | uqkG48XiMMfg4gP0cyAb0QeUBtoGTSn53m1qsBqRklJ7bNMsgMLZbdMWYs1e3bec 17 | tuavvLmHAdfEugbFHh+fL5QYvMfDOrVy3Q== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/e2e/snapshots/coords.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/coords.test.ts` 2 | 3 | The actual snapshot is saved in `coords.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help instal 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl coords [parcels]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | ␊ 18 | Examples:␊ 19 | - Single parcel␊ 20 | - Pass a single argument with the scene coords. This coordinate is also set as the base parcel.␊ 21 | $ dcl coords 0,0␊ 22 | ␊ 23 | - Multiple parcels␊ 24 | - Pass two arguments: the South-West and the North-East parcels. The South-West parcel is also set as the base parcel.␊ 25 | $ dcl coords 0,0 1,1␊ 26 | ␊ 27 | - Customize Base Parcel␊ 28 | - Pass three arguments: the South-West and the North-East parcels, and the parcel to use as a base parcel.␊ 29 | $ dcl coords 0,0 1,1 1,0␊ 30 | ` 31 | -------------------------------------------------------------------------------- /test/unit/utils/gettingPackageJsonAndVersioning.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { getCLIPackageJson } from '../../../src/utils/moduleHelpers' 3 | import { getNodeVersion, getNpmVersion } from '../../../src/utils/nodeAndNpmVersion' 4 | 5 | test('Unit - getCLIPackageJson() - should have userEngines', async (t) => { 6 | const requiredVersion = await getCLIPackageJson<{ 7 | userEngines: { 8 | minNodeVersion: string 9 | minNpmVersion: string 10 | } 11 | }>() 12 | 13 | t.deepEqual(requiredVersion.userEngines, { 14 | minNodeVersion: '14.0.0', 15 | minNpmVersion: '6.0.0' 16 | }) 17 | }) 18 | 19 | test('Unit - npm and node versions - having installed node and npm should return valid values', async (t) => { 20 | const nodeVersion = await getNodeVersion() 21 | const npmVersion = await getNpmVersion() 22 | 23 | t.true(typeof nodeVersion === 'string') 24 | t.true(typeof npmVersion === 'string') 25 | 26 | const majorNodeVersion = parseInt(nodeVersion.split('.')[0]) 27 | const majorNpmVersion = parseInt(npmVersion.split('.')[0]) 28 | 29 | t.true(majorNpmVersion >= 7) 30 | t.true(majorNodeVersion >= 16) 31 | }) 32 | -------------------------------------------------------------------------------- /src/commands/init/help.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | import chalk from 'chalk' 3 | import { getProjectTypes } from './utils' 4 | 5 | export const args = arg({ 6 | '--help': Boolean, 7 | '--project': String, 8 | '--template': String, 9 | '--skip-install': Boolean, 10 | '-h': '--help', 11 | '-p': '--project', 12 | '-t': '--template' 13 | }) 14 | 15 | export const help = () => ` 16 | Usage: ${chalk.bold('dcl init [options]')} 17 | 18 | ${chalk.dim('Options:')} 19 | 20 | -h, --help Displays complete help 21 | -p, --project [type] Choose a projectType (default is scene). It could be any of ${chalk.bold(getProjectTypes())} 22 | 23 | ${chalk.dim('Examples:')} 24 | 25 | - Generate a new Decentraland Scene project in my-project folder 26 | 27 | ${chalk.green('$ dcl init my-project')} 28 | 29 | - Generate a new scene project 30 | 31 | ${chalk.green('$ dcl init --project scene')} 32 | 33 | --skip-install Skip installing dependencies 34 | --template The URL to a template. It must be under the decentraland or decentraland-scenes GitHub organization. 35 | ` 36 | -------------------------------------------------------------------------------- /test/e2e/build.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import test from 'ava' 4 | 5 | import * as buildCmd from '../../src/commands/build' 6 | import { isDebug } from '../../src/utils/env' 7 | import Commando from '../helpers/commando' 8 | import sandbox from '../helpers/sandbox' 9 | import initProject from '../helpers/initProject' 10 | 11 | test('snapshot - dcl help build', (t) => { 12 | t.snapshot(buildCmd.help()) 13 | }) 14 | 15 | function buildProject(dirPath) { 16 | return new Promise((resolve) => { 17 | new Commando(`node ${path.resolve('dist', 'index.js')} build --skip-version-checks`, { 18 | silent: !isDebug(), 19 | workingDir: dirPath, 20 | env: { NODE_ENV: 'development' } 21 | }) 22 | .endWhen(/Project built/) 23 | .on('end', async () => { 24 | resolve() 25 | }) 26 | }) 27 | } 28 | 29 | test('build command', async (t) => { 30 | await sandbox(async (dirPath, done) => { 31 | await initProject(dirPath) 32 | await buildProject(dirPath) 33 | const gameExists = await fs.pathExists(path.resolve(dirPath, 'bin', 'game.js')) 34 | t.true(gameExists) 35 | done() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/helpers/sandbox.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | type CallbackFn = (path: string, done: () => void) => void 5 | 6 | export default function sandbox(fn: CallbackFn) { 7 | return new Promise(async (resolve, reject) => { 8 | const name = 'test-' + (+Date.now()).toString() + (Math.random() * 10).toString() 9 | const dir = path.resolve(process.cwd(), name) 10 | await fs.mkdir(dir) 11 | 12 | const done = () => { 13 | fs.remove(dir, resolve) 14 | } 15 | 16 | try { 17 | fn(dir, done) 18 | } catch (e) { 19 | fs.remove(dir, () => reject(e)) 20 | } 21 | }) 22 | } 23 | 24 | export async function createSandbox(fn: (dirPath: string) => Promise) { 25 | const name = 'test-' + (+Date.now()).toString() + (Math.random() * 10).toString() 26 | const dir = path.resolve(process.cwd(), name) 27 | await fs.mkdir(dir) 28 | try { 29 | await fn(dir) 30 | } finally { 31 | await removeSandbox(dir) 32 | } 33 | } 34 | 35 | export function removeSandbox(dirPath: string) { 36 | return new Promise((resolve, reject) => { 37 | fs.remove(dirPath, (err) => (err ? reject(err) : resolve())) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /test/e2e/help.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | 4 | import commands from '../../src/commands' 5 | import { isDebug } from '../../src/utils/env' 6 | import Commando from '../helpers/commando' 7 | 8 | test('snapshot - dcl commands', (t) => { 9 | t.snapshot(commands) 10 | }) 11 | 12 | test('help command', async (t) => { 13 | // dcl help 14 | const allDataDclHelpPromise = new Promise((resolve) => { 15 | let allData = '' 16 | new Commando( 17 | `node ${path.resolve('dist', 'index.js')} help`, 18 | { silent: !isDebug(), workingDir: '.', env: { NODE_ENV: 'development' } }, 19 | (data) => (allData += data) 20 | ).on('end', async () => { 21 | resolve(allData) 22 | }) 23 | }) 24 | 25 | // dcl # no command 26 | const allDataDclPromise = new Promise((resolve) => { 27 | let allData = '' 28 | new Commando( 29 | `node ${path.resolve('dist', 'index.js')}`, 30 | { silent: !isDebug(), workingDir: '.', env: { NODE_ENV: 'development' } }, 31 | (data) => (allData += data) 32 | ).on('end', async () => { 33 | resolve(allData) 34 | }) 35 | }) 36 | 37 | const [allDataDclHelp, allDataDcl] = await Promise.all([allDataDclHelpPromise, allDataDclPromise]) 38 | t.is(allDataDcl, allDataDclHelp) 39 | t.snapshot(allDataDcl) 40 | }) 41 | -------------------------------------------------------------------------------- /test/e2e/snapshots/info.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/info.test.ts` 2 | 3 | The actual snapshot is saved in `info.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help info 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl info [target] [options]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | -b, --blockchain Retrieve information directly from the blockchain instead of Decentraland remote API␊ 18 | -n, --network Choose between mainnet and sepolia (default 'mainnet')␊ 19 | ␊ 20 | ␊ 21 | Examples:␊ 22 | ␊ 23 | - Get information from the LAND located at "-12, 40"␊ 24 | ␊ 25 | $ dcl info -12,40␊ 26 | ␊ 27 | - Get information from the estate with ID "5" directly from blockchain provider␊ 28 | ␊ 29 | $ dcl info 5 --blockchain␊ 30 | ␊ 31 | - Get information from the address 0x8bed95d830475691c10281f1fea2c0a0fe51304b"␊ 32 | ␊ 33 | $ dcl info 0x8bed95d830475691c10281f1fea2c0a0fe51304b␊ 34 | ` 35 | 36 | ## E2E - info command 37 | 38 | > Snapshot 1 39 | 40 | '' 41 | -------------------------------------------------------------------------------- /test/e2e/snapshots/deploy.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/deploy.test.ts` 2 | 3 | The actual snapshot is saved in `deploy.test.ts.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## snapshot - dcl help deploy-deprecated 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl deploy [path] [options]␊ 13 | ␊ 14 | Options:␊ 15 | ␊ 16 | -h, --help Displays complete help␊ 17 | -y, --yes Skip confirmations and proceed to upload␊ 18 | -l, --https Use self-signed localhost certificate to use HTTPs at linking app (required for ledger users)␊ 19 | -f, --force-upload Upload all files to the content server␊ 20 | -n, --network Choose between mainnet and ropsten (default 'mainnet') only available with env DCL_PRIVATE_KEY␊ 21 | ␊ 22 | Examples:␊ 23 | ␊ 24 | - Deploy a Decentraland Scene project in folder my-project␊ 25 | ␊ 26 | $ dcl deploy my-project␊ 27 | ␊ 28 | - Deploy a Decentraland Scene from a CI or an automated context␊ 29 | ␊ 30 | $ dcl deploy -y␊ 31 | ␊ 32 | - Deploy a Decentraland Scene project using a ledger hardware wallet␊ 33 | ␊ 34 | $ dcl deploy --https␊ 35 | ` 36 | -------------------------------------------------------------------------------- /src/utils/getProjectFilePaths.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import ignore from 'ignore' 4 | 5 | export default async function getProjectFilePaths(dir: string, ignoreFileContent?: string): Promise { 6 | const fileNames = (ignore as any)() 7 | .add(ignoreFileContent) 8 | .filter(await fs.readdir(dir)) as string[] 9 | const filePaths = fileNames.map((fileName) => path.resolve(dir, fileName)) 10 | const stats = await Promise.all(filePaths.map((filePath) => fs.stat(filePath))) 11 | const files: string[] = [] 12 | const pendingPromises: Promise[] = [] 13 | 14 | stats.forEach(async (stat, i) => { 15 | if (stat.isDirectory()) { 16 | const promise = new Promise((resolve, reject) => { 17 | getProjectFilePaths(filePaths[i], ignoreFileContent) 18 | .then((resolvedFilePaths) => { 19 | const finals = resolvedFilePaths.map((f) => path.join(fileNames[i], f)) 20 | resolve(finals) 21 | }) 22 | .catch(reject) 23 | }) 24 | 25 | pendingPromises.push(promise) 26 | } else { 27 | files.push(fileNames[i]) 28 | } 29 | }) 30 | 31 | const pResults = (await Promise.all(pendingPromises)).reduce((acc: string[], r: any) => { 32 | acc.push(...r) 33 | return acc 34 | }, []) 35 | 36 | return [...files, ...pResults] 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/spinner.ts: -------------------------------------------------------------------------------- 1 | import ora, { Ora } from 'ora' 2 | 3 | let spinner: Ora | null = null 4 | 5 | export function create(message: string) { 6 | if (!process.stdout.isTTY && process.env.DEBUG) { 7 | return console.log(message) 8 | } 9 | 10 | if (spinner) { 11 | spinner.succeed() 12 | } 13 | 14 | spinner = ora(message).start() 15 | } 16 | 17 | export function fail(message?: string) { 18 | if (!process.stdout.isTTY && process.env.DEBUG && message) { 19 | return console.log(message) 20 | } 21 | 22 | if (spinner) { 23 | spinner.fail(message) 24 | } 25 | } 26 | 27 | export function warn(message?: string) { 28 | if (!process.stdout.isTTY && process.env.DEBUG && message) { 29 | return console.log(message) 30 | } 31 | 32 | if (spinner) { 33 | spinner.warn(message) 34 | spinner = null 35 | } 36 | } 37 | 38 | export function info(message?: string) { 39 | if (!process.stdout.isTTY && process.env.DEBUG && message) { 40 | return console.log(message) 41 | } 42 | 43 | if (spinner) { 44 | spinner.info(message) 45 | spinner = null 46 | } 47 | } 48 | 49 | export function succeed(message?: string) { 50 | if (!process.stdout.isTTY && process.env.DEBUG && message) { 51 | return console.log(message) 52 | } 53 | 54 | if (spinner) { 55 | spinner.succeed(message) 56 | spinner = null 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/e2e/install.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test from 'ava' 3 | 4 | import * as installCmd from '../../src/commands/install' 5 | import sandbox from '../helpers/sandbox' 6 | import initProject from '../helpers/initProject' 7 | import Commando from '../helpers/commando' 8 | import { isDebug } from '../../src/utils/env' 9 | import { readJSON } from '../../src/utils/filesystem' 10 | 11 | function installCommand(dirPath: string, library: string = '') { 12 | return new Promise((resolve) => { 13 | const cmd = new Commando(`node ${path.resolve('dist', 'index.js')} install ${library}`, { 14 | silent: !isDebug(), 15 | workingDir: dirPath, 16 | env: { NODE_ENV: 'development' } 17 | }) 18 | 19 | cmd.on('end', async () => { 20 | resolve() 21 | }) 22 | }) 23 | } 24 | 25 | test('snapshot - dcl help instal', (t) => { 26 | t.snapshot(installCmd.help()) 27 | }) 28 | 29 | test('install a package', async (t) => { 30 | await sandbox(async (dirPath, done) => { 31 | const packageToInstall = '@dcl/ecs-scene-utils' 32 | await initProject(dirPath) 33 | await installCommand(dirPath, packageToInstall) 34 | 35 | const packageJson = await readJSON<{ bundledDependencies: string[] }>(path.resolve(dirPath, 'package.json')) 36 | t.true(packageJson.bundledDependencies.includes(packageToInstall)) 37 | done() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/src/game.ts: -------------------------------------------------------------------------------- 1 | /// --- Set up a system --- 2 | 3 | class RotatorSystem { 4 | // this group will contain every entity that has a Transform component 5 | group = engine.getComponentGroup(Transform) 6 | 7 | update(dt: number) { 8 | // iterate over the entities of the group 9 | for (const entity of this.group.entities) { 10 | // get the Transform component of the entity 11 | const transform = entity.getComponent(Transform) 12 | 13 | // mutate the rotation 14 | transform.rotate(Vector3.Up(), dt * 10) 15 | } 16 | } 17 | } 18 | 19 | // Add a new instance of the system to the engine 20 | engine.addSystem(new RotatorSystem()) 21 | 22 | /// --- Spawner function --- 23 | 24 | function spawnCube(x: number, y: number, z: number) { 25 | // create the entity 26 | const cube = new Entity() 27 | 28 | // add a transform to the entity 29 | cube.addComponent(new Transform({ position: new Vector3(x, y, z) })) 30 | 31 | // add a shape to the entity 32 | cube.addComponent(new BoxShape()) 33 | 34 | // add the entity to the engine 35 | engine.addEntity(cube) 36 | 37 | return cube 38 | } 39 | 40 | /// --- Spawn a cube --- 41 | 42 | const cube = spawnCube(8, 1, 8) 43 | 44 | cube.addComponent( 45 | new OnClick(() => { 46 | cube.getComponent(Transform).scale.z *= 1.1 47 | cube.getComponent(Transform).scale.x *= 0.9 48 | 49 | spawnCube(Math.random() * 8 + 1, Math.random() * 8, Math.random() * 8 + 1) 50 | }) 51 | ) 52 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | import extract from 'extract-zip' 2 | import { rmdirSync } from 'fs' 3 | import { move, readdir, remove, writeFile } from 'fs-extra' 4 | import path from 'path' 5 | import fetch from 'node-fetch' 6 | 7 | export const downloadFile = async function (url: string, dest: string) { 8 | const data = await (await fetch(url)).arrayBuffer() 9 | await writeFile(dest, Buffer.from(data)) 10 | } 11 | 12 | export const downloadRepoZip = async function (url: string, dest: string) { 13 | const zipFilePath = path.resolve(dest, 'temp-zip-project.zip') 14 | await downloadFile(url, zipFilePath) 15 | 16 | const oldFiles = await readdir(dest) 17 | 18 | try { 19 | await extract(zipFilePath, { dir: dest }) 20 | } catch (err) { 21 | console.log(`Couldn't extract the zip of the repository.`, err) 22 | throw err 23 | } 24 | 25 | const newFiles = await readdir(dest) 26 | 27 | const directoryCreated = newFiles.filter((value) => !oldFiles.includes(value)) 28 | 29 | if (directoryCreated.length !== 1) { 30 | throw new Error('Please, make sure not to modify the directory while the example repository is downloading.') 31 | } 32 | 33 | const extractedPath = path.resolve(dest, directoryCreated[0]) 34 | const filesToMove = await readdir(extractedPath) 35 | 36 | for (const filePath of filesToMove) { 37 | await move(path.resolve(extractedPath, filePath), path.resolve(dest, filePath)) 38 | } 39 | 40 | rmdirSync(extractedPath) 41 | await remove(zipFilePath) 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/init/repositories.ts: -------------------------------------------------------------------------------- 1 | export const repos = { 2 | scenes: [ 3 | { 4 | title: 'Cube spawner', 5 | url: 'https://github.com/decentraland-scenes/cube-spawner/archive/refs/heads/main.zip' 6 | }, 7 | { 8 | title: 'Basic interactions', 9 | url: 'https://github.com/decentraland-scenes/Basic-Interactions/archive/refs/heads/master.zip' 10 | }, 11 | { 12 | title: 'Moving platforms', 13 | url: 'https://github.com/decentraland-scenes/moving-platforms/archive/refs/heads/master.zip' 14 | }, 15 | { 16 | title: 'Video streaming', 17 | url: 'https://github.com/decentraland-scenes/video-streaming/archive/refs/heads/main.zip' 18 | }, 19 | { 20 | title: 'Museum', 21 | url: 'https://github.com/decentraland-scenes/museum_template/archive/refs/heads/main.zip' 22 | }, 23 | { 24 | title: 'Wearables Store', 25 | url: 'https://github.com/decentraland-scenes/store-template/archive/refs/heads/main.zip' 26 | }, 27 | { 28 | title: 'Lazy loading', 29 | url: 'https://github.com/decentraland-scenes/lazy-loading/archive/refs/heads/main.zip' 30 | } 31 | ], 32 | library: 'https://github.com/decentraland/sdk-library/archive/refs/heads/main.zip', 33 | portableExperience: 'https://github.com/decentraland/portable-experience-sample/archive/refs/heads/main.zip', 34 | smartItem: 'https://github.com/decentraland/smart-item-sample/archive/refs/heads/main.zip' 35 | } 36 | 37 | export default repos 38 | -------------------------------------------------------------------------------- /src/commands/init/scene.ts: -------------------------------------------------------------------------------- 1 | import inquirer, { QuestionCollection } from 'inquirer' 2 | import repositories from './repositories' 3 | import { ErrorType, fail } from '../../utils/errors' 4 | import { InitOption } from './types' 5 | 6 | export async function sceneOptions(): Promise { 7 | const sceneChoices = [ 8 | ...repositories.scenes.map((repo, index) => ({ 9 | name: `(${index + 1}) ${repo.title}`, 10 | value: repo.url 11 | })), 12 | { 13 | name: 'Paste a repository URL', 14 | value: 'write-repository' 15 | } 16 | ] 17 | 18 | const projectTypeList: QuestionCollection = [ 19 | { 20 | type: 'list', 21 | name: 'scene', 22 | message: 'Choose a scene', 23 | choices: sceneChoices 24 | } 25 | ] 26 | 27 | const answers = await inquirer.prompt(projectTypeList) 28 | 29 | if (answers.scene === 'write-repository') { 30 | const answers = await inquirer.prompt([ 31 | { 32 | type: 'input', 33 | name: 'url', 34 | message: 'Write the repository URL:' 35 | } 36 | ]) 37 | return { 38 | type: 'scene', 39 | value: answers.url 40 | } 41 | } else if (answers.scene) { 42 | const choice = sceneChoices.find((item) => item.value === answers.scene) 43 | if (choice) { 44 | return { 45 | type: 'scene', 46 | value: answers.scene 47 | } 48 | } 49 | } 50 | 51 | fail(ErrorType.INIT_ERROR, `Couldn't get a valid scene-level choice. Try to select a valid one.`) 52 | return {} as any 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/project.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const SCENE_FILE = 'scene.json' 4 | export const PACKAGE_FILE = 'package.json' 5 | export const GITIGNORE_FILE = 'gitignore' 6 | export const NPMRC_FILE = 'npmrc' 7 | export const ESTLINTRC_FILE = 'eslintrc.json' 8 | export const ASSET_JSON_FILE = 'asset.json' 9 | export const WEARABLE_JSON_FILE = 'wearable.json' 10 | export const DCLIGNORE_FILE = '.dclignore' 11 | 12 | /** 13 | * Composes the path to the `scene.json` file based on the provided path. 14 | * @param dir The path to the directory containing the scene file. 15 | */ 16 | export function getSceneFilePath(dir: string): string { 17 | return path.resolve(dir, SCENE_FILE) 18 | } 19 | 20 | /** 21 | * Composes the path to the `package.json` file based on the provided path. 22 | * @param dir The path to the directory containing the package.json file. 23 | */ 24 | export function getPackageFilePath(dir: string): string { 25 | return path.resolve(dir, PACKAGE_FILE) 26 | } 27 | 28 | /** 29 | * Composes the path to the `.dclignore` file based on the provided path. 30 | * @param dir The path to the directory containing the .dclignore file. 31 | */ 32 | export function getIgnoreFilePath(dir: string): string { 33 | return path.resolve(dir, DCLIGNORE_FILE) 34 | } 35 | 36 | /** 37 | * Returns the path to the node_modules directory. 38 | * @param dir The path to the directory containing the node_modules directory. 39 | */ 40 | export function getNodeModulesPath(dir: string): string { 41 | return path.resolve(dir, 'node_modules') 42 | } 43 | -------------------------------------------------------------------------------- /src/sceneJson/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Scene } from '@dcl/schemas' 3 | 4 | import * as spinner from '../utils/spinner' 5 | 6 | function checkMissingOrDefault>(obj: T, defaults: T) { 7 | const missingKeys = Object.entries(defaults).reduce((acc: string[], [key, value]) => { 8 | return obj[key] && obj[key] !== value ? acc : acc.concat(key) 9 | }, []) 10 | return missingKeys 11 | } 12 | 13 | export function validateScene(sceneJson: Scene, log: boolean = false): boolean { 14 | log && spinner.create('Validating scene.json') 15 | 16 | const validScene = Scene.validate(sceneJson) 17 | if (!validScene) { 18 | const error = (Scene.validate.errors || []).map((a) => `${a.data} ${a.message}`).join('') 19 | 20 | log && spinner.fail(`Invalid scene.json: ${error}`) 21 | return false 22 | } 23 | 24 | const defaults: Scene['display'] = { 25 | title: 'DCL Scene', 26 | description: 'My new Decentraland project', 27 | navmapThumbnail: 'images/scene-thumbnail.png' 28 | } 29 | const sceneDisplay = sceneJson.display || {} 30 | 31 | const missingKeys = checkMissingOrDefault>(sceneDisplay, defaults) 32 | 33 | if (log) { 34 | if (missingKeys.length) { 35 | spinner.warn(`Don't forget to update your scene.json metadata: [${missingKeys.join(', ')}] 36 | ${chalk.underline.bold('https://docs.decentraland.org/development-guide/scene-metadata/')}`) 37 | } else { 38 | spinner.succeed() 39 | } 40 | } 41 | 42 | return !missingKeys.length 43 | } 44 | -------------------------------------------------------------------------------- /src/project/updateBundleDependenciesField.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | import { readJSON, PackageJson } from '../utils/filesystem' 5 | import { getDecentralandDependencies, getDependencies } from '../utils/installedDependencies' 6 | import * as spinner from '../utils/spinner' 7 | 8 | export default async function ({ workDir }: { workDir: string }) { 9 | try { 10 | spinner.create('Checking decentraland libraries') 11 | 12 | const packageJsonDir = path.resolve(workDir, 'package.json') 13 | const packageJSON = await readJSON(packageJsonDir) 14 | const pkgDependencies = getDependencies(packageJSON) 15 | const decentralandDependencies = await getDecentralandDependencies( 16 | { ...pkgDependencies.dependencies, ...pkgDependencies.devDependencies }, 17 | workDir 18 | ) 19 | 20 | const missingBundled = !!decentralandDependencies.find( 21 | (name) => !pkgDependencies.bundledDependencies.includes(name) 22 | ) 23 | 24 | if (missingBundled) { 25 | const allBundledDependencies = new Set([...pkgDependencies.bundledDependencies, ...decentralandDependencies]) 26 | const { bundledDependencies, bundleDependencies, ...packageJsonProps } = packageJSON 27 | const newPackage = { 28 | ...packageJsonProps, 29 | bundledDependencies: Array.from(allBundledDependencies) 30 | } 31 | 32 | await fs.writeFile(packageJsonDir, JSON.stringify(newPackage, null, 2)) 33 | } 34 | spinner.succeed() 35 | } catch (e) { 36 | spinner.fail() 37 | throw e 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/controllers/debugger.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@well-known-components/http-server' 2 | import { upgradeWebSocketResponse } from '@well-known-components/http-server/dist/ws' 3 | import { fork } from 'child_process' 4 | import WebSocket from 'ws' 5 | import { PreviewComponents } from '../Preview' 6 | 7 | export function setupDebuggingAdapter(components: PreviewComponents, router: Router) { 8 | router.get('/_scene/debug-adapter', async (ctx) => { 9 | if (ctx.request.headers.get('upgrade') === 'websocket') { 10 | return upgradeWebSocketResponse((ws: WebSocket) => { 11 | if (ws.protocol === 'dcl-scene') { 12 | const file = require.resolve('dcl-node-runtime') 13 | 14 | const theFork = fork(file, [], { 15 | // enable two way IPC 16 | stdio: [0, 1, 2, 'ipc'], 17 | cwd: process.cwd() 18 | }) 19 | 20 | console.log(`> Creating scene fork #` + theFork.pid) 21 | 22 | theFork.on('close', () => { 23 | if (ws.readyState === ws.OPEN) { 24 | ws.close() 25 | } 26 | }) 27 | theFork.on('message', (message) => { 28 | if (ws.readyState === ws.OPEN) { 29 | ws.send(message) 30 | } 31 | }) 32 | ws.on('message', (data) => theFork.send(data.toString())) 33 | ws.on('close', () => { 34 | console.log('> Killing fork #' + theFork.pid) 35 | theFork.kill() 36 | }) 37 | } else ws.close() 38 | }) 39 | } 40 | return { status: 201 } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /test/e2e/snapshots/world-acl.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/world-acl.test.ts` 2 | 3 | The actual snapshot is saved in `world-acl.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl help world-acl 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | Usage: dcl world-acl [world-name] SUBCOMMAND [options]␊ 13 | ␊ 14 | Sub commands:␊ 15 | show List all addresses allowed to deploy a scene to a specified world.␊ 16 | grant addr Grant permission to new address to deploy a scene to a specified world.␊ 17 | revoke addr Remove permission for given address to deploy a scene to a specified world.␊ 18 | ␊ 19 | ␊ 20 | Options:␊ 21 | ␊ 22 | -h, --help Displays complete help␊ 23 | -p, --port [port] Select a custom port for the linker app (for signing with browser wallet)␊ 24 | -t, --target-content [url] Specifies the base URL for the target Worlds Content Server. Example: 'https://worlds-content-server.decentraland.org'.␊ 25 | ␊ 26 | Examples:␊ 27 | - Show which addresses were given permission to deploy name.dcl.eth␊ 28 | $ dcl world-acl name.dcl.eth show␊ 29 | ␊ 30 | - Grant address 0x1 permission to deploy name.dcl.eth␊ 31 | $ dcl world-acl name.dcl.eth grant 0x1␊ 32 | ␊ 33 | - Revoke address 0x1 permission to deploy name.dcl.eth␊ 34 | $ dcl world-acl name.dcl.eth revoke 0x1␊ 35 | ` 36 | -------------------------------------------------------------------------------- /certs/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4LwrwmiCd9dCV 3 | Ids9+pAB7P9+VTfW5eEck4OqyiPQxvjJFP4jrve/FcxrRwr1nnM64fALRrcVSRjp 4 | kh9Uv+AV2Z0QchIhp5nd/ELjjRxbB57mtDBhUvRqIRtHnZHyJ73edfAoXdEwwp// 5 | pzCBTzxOks5YUDyNA8K2OegZ7PTG5KDGLSeayUCdVb/4hHYr+sJJrcRMI8QMmHSX 6 | JKoqMw5X78R93AFwQTz9VGIMJai4I775cleKGDN/48YdgSXdFOMbP0ccmUag/dIG 7 | 4+p9bqb+EQ97Dhzfzq5SSGtmY1gGAAGWjAY6yl6oULLgdgkMv2zJTES61AGom5Qa 8 | YCp3ACqXAgMBAAECggEATybISMWzpq5wyOYX1fbL1EgJND1jFdMMfr9WIhtkcOBR 9 | IfkHjkYu6ctwYdnR9+P0GSXnhrEJFPio4BePp1gd8MXoHZ0n9ZaVJTS3ehq2SNhp 10 | jTN6ZxjDOKiplIk+oxY5HlUItBA9SfsZz0AGNEvc2td0Hbe9jcxD7RPNjvap6p6z 11 | Zrul8SIPydjEBChHUOF/MOaK86l0Nr5qpqsU+7YZwf7XfZ5u5vTWR3ZAGt3E1Tsq 12 | 3s7bVgHXKsTU/ttrP7jgYkgP7YtO5CkK5WLUA3Myo0P4KzYT7yrht3ueJ8bsZiUH 13 | iSBbNC26A4TUluw6AvTdS97D3zZ78RhTCuKpQ1TjoQKBgQDZQd75as8k5adq/mMf 14 | rZyUygwyuG3Yw95T4oNVE9pyFC11Zs7sY3V9MSpfU24j3d61nl+n0xM99wcqyPaC 15 | EeFbQ2QMtkt+rDlivpYOy+0U98H5JMkG8cMShG0neV7iDj/xgCYdE87wrjq/ZL1w 16 | yZt72LVtTLUwaU6QPsWZV526ZQKBgQDZB1BId60FGnZAEAPtu/z0W4w6QqCmh0at 17 | Sz9ixOAISOqd8oebBLC5QBHx/It0iWiOz0uasR5c7rz6Hp5BFpuNH3EvzgFQtIXC 18 | 4oVmtRzf9G7n83amaDh3iII+shtdc9QnX6lQ/LmjHyVBzTq2l7htR3L1QzIq8ouy 19 | IZQOr8fjSwKBgCrvsblpOnb4TBYBIGXqUb+2DqMXf94PF6lMYtg1jD5vbmx9XPeq 20 | 0FVlmhIs0t+TwafzHMR2Gp9saqYyAUXDct4ue19nx5PJRa4WLGHQO5KhRFyQwIn2 21 | za6jLU9X1UCnwEtiICYu+/7k8AdTSX042tmnAnQPbN+ccEJhpXugrTNhAoGBAL7Q 22 | fFjKyBfllTRsIFgkYZoi557Nt9vSsmRo9XkMqgD+wzFN7MZyEENAPsbpCV/T9Fcj 23 | kVCYC58f1I2A4BlQHEGu9GBYmrVvku+vJCUSdim+CsjrOVXD6mnGXuVqyT6YOV7I 24 | 7+Ah48G0/5fkLowdx2xlVoCnrPMvv31FopxrFq8TAoGBAJDWabVQUTTT1jNfzzCR 25 | FYEXX19sQ5Y6DW1YRLkxa3uq7IyTO9VNAMfi4DrzLBTQ5zBOWTALUi6q+NgTQAgg 26 | As9AwNRxwDEKyePmmzDKsrmIORQvAFumN7ONRVKXwWyY4w3sH9L2R4gFwl0YdSQe 27 | V5IS23MsBA9XAphuWCiHeKjU 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/utils/installedDependencies.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | import { readJSON, PackageJson } from './filesystem' 5 | 6 | type DecentralandPackage = PackageJson<{ decentralandLibrary: string }> 7 | 8 | type Dependencies = Pick 9 | 10 | const parseBundled = (dependencies: unknown) => { 11 | if (dependencies instanceof Array) { 12 | return dependencies 13 | } 14 | return [] 15 | } 16 | 17 | export function getDependencies(packageJSON: PackageJson): Required { 18 | const { 19 | bundleDependencies = [], 20 | bundledDependencies = [], 21 | dependencies = {}, 22 | devDependencies = {}, 23 | peerDependencies = {} 24 | } = packageJSON 25 | const bundled = [...parseBundled(bundleDependencies), ...parseBundled(bundledDependencies)].filter( 26 | (b) => typeof b === 'string' 27 | ) 28 | 29 | return { 30 | dependencies, 31 | devDependencies, 32 | peerDependencies, 33 | bundledDependencies: bundled 34 | } 35 | } 36 | 37 | function getPath(workDir: string, name: string) { 38 | return path.resolve(workDir, 'node_modules', name, 'package.json') 39 | } 40 | 41 | export async function getDecentralandDependencies( 42 | dependencies: Record, 43 | workDir: string 44 | ): Promise { 45 | const dependenciesName = [] 46 | for (const dependency of Object.keys(dependencies)) { 47 | const modulePath = getPath(workDir, dependency) 48 | 49 | if (fs.pathExistsSync(modulePath)) { 50 | const pkgJson = await readJSON(modulePath) 51 | if (pkgJson.decentralandLibrary && pkgJson.name && pkgJson.version) { 52 | dependenciesName.push(dependency) 53 | } 54 | } 55 | } 56 | 57 | return dependenciesName 58 | } 59 | -------------------------------------------------------------------------------- /test/fixtures/ecs-compiled/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcl-project", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "commander": { 8 | "version": "2.17.1", 9 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", 10 | "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", 11 | "dev": true 12 | }, 13 | "decentraland-ecs": { 14 | "version": "5.0.0", 15 | "resolved": "https://registry.npmjs.org/decentraland-ecs/-/decentraland-ecs-5.0.0.tgz", 16 | "integrity": "sha512-ls2cS1bZO4VACg25ox3Rq6Qb47O2v2nZICkEHeHIIgIZqzzFrL++OQiZNpHXnePzPxFm/kgeEUlgBV0UYhPigw==", 17 | "dev": true, 18 | "requires": { 19 | "typescript": "^3.2.2", 20 | "uglify-js": "^3.4.9" 21 | } 22 | }, 23 | "source-map": { 24 | "version": "0.6.1", 25 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 26 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 27 | "dev": true 28 | }, 29 | "typescript": { 30 | "version": "3.2.4", 31 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", 32 | "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", 33 | "dev": true 34 | }, 35 | "uglify-js": { 36 | "version": "3.4.9", 37 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", 38 | "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", 39 | "dev": true, 40 | "requires": { 41 | "commander": "~2.17.1", 42 | "source-map": "~0.6.1" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #### Please read the guidelines in https://github.com/decentraland/standards 2 | 3 | # Security 4 | 5 | Sign all your commits with GPG 6 | 7 | - https://github.com/blog/2144-gpg-signature-verification 8 | - https://help.github.com/articles/generating-a-new-gpg-key/ 9 | - https://help.github.com/articles/signing-commits-using-gpg/ 10 | 11 | # Commit messages 12 | 13 | We do [semantic commits](https://seesparkbox.com/foundry/semantic_commit_messages) 14 | 15 | ``` 16 | feat: add hat wobble 17 | ^--^ ^------------^ 18 | | | 19 | | +-> Summary in present tense. 20 | | 21 | +-------> Type: chore, docs, feat, fix, refactor, style, or test. 22 | ``` 23 | 24 | 25 | Examples: 26 | 27 | ``` 28 | chore: add Oyster build script 29 | ``` 30 | ``` 31 | docs: explain hat wobble 32 | ``` 33 | ``` 34 | feat: add beta sequence, implements #332 35 | ``` 36 | ``` 37 | fix: remove broken confirmation message, closes #123 38 | ``` 39 | ``` 40 | refactor: share logic between 4d3d3d3 and flarhgunnstow 41 | ``` 42 | ``` 43 | style: convert tabs to spaces 44 | ``` 45 | ``` 46 | test: ensure Tayne retains clothing 47 | ``` 48 | 49 | ## Allowed `` values: 50 | * `feat` new feature 51 | * `fix` bug fix 52 | * `docs` changes to the documentation 53 | * `style` formatting, linting, etc; no production code change 54 | * `refactor` refactoring production code, eg. renaming a variable 55 | * `test` adding missing tests, refactoring tests; no production code change 56 | * `chore` updating build tasks etc; no production code change 57 | 58 | # Merge commits 59 | 60 | Avoid `Merge branch 'a' into 'master'` commit messages. Rebase when possible 61 | 62 | # Code style 63 | 64 | Every commit should be linted using the command `npm run lint:fix` 65 | 66 | For TSLint refer to: https://github.com/decentraland/standards/blob/master/standards/tslint-configuration.md 67 | 68 | For TypeScript (and react specific) guidelines refer to https://github.com/decentraland/standards/blob/master/standards/react-redux.md 69 | -------------------------------------------------------------------------------- /src/utils/catalystPointers.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@dcl/schemas' 2 | import fetch from 'node-fetch' 3 | 4 | export type DAOCatalyst = { 5 | baseUrl: string 6 | owner: string 7 | id: string 8 | } 9 | 10 | type CatalystInfo = { 11 | url: string 12 | timestamp: number 13 | entityId: string 14 | } 15 | 16 | export type Network = 'mainnet' | 'sepolia' 17 | 18 | export async function daoCatalysts(network: Network = 'mainnet'): Promise> { 19 | const tld = network === 'mainnet' ? 'org' : 'zone' 20 | const resp = await (await fetch(`https://peer.decentraland.${tld}/lambdas/contracts/servers`)).json() 21 | return resp as DAOCatalyst[] 22 | } 23 | 24 | export async function fetchEntityByPointer( 25 | baseUrl: string, 26 | pointers: string[] 27 | ): Promise<{ 28 | baseUrl: string 29 | deployments: Entity[] 30 | }> { 31 | if (pointers.length === 0) 32 | return { 33 | baseUrl, 34 | deployments: [] 35 | } 36 | 37 | const activeEntities = baseUrl + '/content/entities/active' 38 | 39 | const response = await fetch(activeEntities, { 40 | method: 'post', 41 | headers: { 'content-type': 'application/json', connection: 'close' }, 42 | body: JSON.stringify({ pointers }) 43 | }) 44 | 45 | const deployments: Entity[] = response.ok ? ((await response.json()) as Entity[]) : [] 46 | 47 | return { 48 | baseUrl, 49 | deployments 50 | } 51 | } 52 | 53 | export async function getPointers(pointer: string, network: Network = 'mainnet') { 54 | const catalysts = await daoCatalysts(network) 55 | const catalystInfo: CatalystInfo[] = [] 56 | 57 | for (const { baseUrl } of catalysts) { 58 | try { 59 | const result = await fetchEntityByPointer(baseUrl, [pointer]) 60 | const timestamp = result.deployments[0]?.timestamp 61 | const entityId = result.deployments[0]?.id || '' 62 | 63 | catalystInfo.push({ timestamp, entityId, url: baseUrl }) 64 | } catch (err: any) { 65 | console.log('Error fetching catalyst pointers', err) 66 | } 67 | } 68 | 69 | return catalystInfo 70 | } 71 | -------------------------------------------------------------------------------- /test/integration/Project.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { Project } from '../../src/lib/Project' 4 | import sandbox from '../helpers/sandbox' 5 | import { setupFilesystem } from './helpers' 6 | 7 | test('Integration - Project', async (t) => { 8 | await sandbox(async (dirPath, done) => { 9 | await setupFilesystem(dirPath, [ 10 | { 11 | path: '.decentraland/test.js', 12 | content: 'console.log()' 13 | }, 14 | { 15 | path: 'test.js', 16 | content: 'console.log()' 17 | }, 18 | { 19 | path: 'models/test.fbx', 20 | content: '...' 21 | }, 22 | { 23 | path: 'src/index.ts', 24 | content: 'console.log()' 25 | }, 26 | { 27 | path: 'src/package.json', 28 | content: '{}' 29 | }, 30 | { 31 | path: 'src/node_modules/example/index.js', 32 | content: 'console.log()' 33 | }, 34 | { 35 | path: 'package.json', 36 | content: '{}' 37 | }, 38 | { 39 | path: 'tsconfig.json', 40 | content: '{}' 41 | }, 42 | { 43 | path: 'scene.json', 44 | content: '{}' 45 | }, 46 | { 47 | path: 'scene.xml', 48 | content: '' 49 | }, 50 | { 51 | path: '.dclignore', 52 | content: `.*\npackage.json\npackage-lock.json\nyarn-lock.json\nbuild.json\ntsconfig.json\ntslint.json\nnode_modules/\n*.ts\n*.tsx\ndist/` 53 | } 54 | ]) 55 | const project = new Project(dirPath) 56 | const [files, need, tsProject] = await Promise.all([ 57 | project.getFiles({ 58 | ignoreFiles: `.*\npackage.json\npackage-lock.json\nyarn-lock.json\nbuild.json\ntsconfig.json\ntslint.json\nnode_modules/\n*.ts\n*.tsx\ndist/` 59 | }), 60 | project.needsDependencies(), 61 | project.isTypescriptProject() 62 | ]) 63 | 64 | t.deepEqual( 65 | files.map((f) => f.path), 66 | ['models/test.fbx', 'scene.json', 'scene.xml', 'test.js'] 67 | ) 68 | t.true(need) 69 | t.true(tsProject) 70 | done() 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/commands/install.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | import chalk from 'chalk' 3 | import updateBundleDependenciesField from '../project/updateBundleDependenciesField' 4 | import { spawn } from 'child_process' 5 | import { npm } from './../utils/moduleHelpers' 6 | import * as spinner from '../utils/spinner' 7 | import { createWorkspace } from '../lib/Workspace' 8 | import { fail } from 'assert' 9 | 10 | export const help = () => ` 11 | Usage: ${chalk.bold('dcl install [package]')} 12 | 13 | ${chalk.dim('Options:')} 14 | 15 | -h, --help Displays complete help 16 | 17 | ${chalk.dim('Examples:')} 18 | 19 | - Install a new package 20 | 21 | ${chalk.green('$ dcl install package-example')} 22 | 23 | - Check the Decentraland libraries used are in bundleDependencies 24 | 25 | ${chalk.green('$ dcl install')} 26 | ` 27 | 28 | const spawnNpmInstall = (args: any): Promise => { 29 | return new Promise((resolve, reject) => { 30 | spinner.create(`npm ${args.join(' ')}\n`) 31 | 32 | const child = spawn(npm, args, { 33 | shell: true, 34 | cwd: process.cwd(), 35 | env: { ...process.env, NODE_ENV: '' } 36 | }) 37 | 38 | child.stdout.pipe(process.stdout) 39 | child.stderr.pipe(process.stderr) 40 | 41 | child.on('close', (code) => { 42 | if (code !== 0) { 43 | spinner.fail() 44 | reject( 45 | new Error( 46 | `${chalk.bold(`npm ${args.join(' ')}`)} exited with code ${code}. Please try running the command manually` 47 | ) 48 | ) 49 | } else { 50 | spinner.succeed() 51 | resolve() 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | export async function main() { 58 | const args = arg({ 59 | '--help': Boolean, 60 | '-h': '--help' 61 | }) 62 | 63 | const workingDir = process.cwd() 64 | const workspace = createWorkspace({ workingDir }) 65 | 66 | if (!workspace.isSingleProject()) { 67 | fail("You should't use `dcl install` in a workspace folder.") 68 | } 69 | 70 | await spawnNpmInstall(args._) 71 | 72 | await updateBundleDependenciesField({ workDir: workingDir }) 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/controllers/bff.ts: -------------------------------------------------------------------------------- 1 | import { handleSocketLinearProtocol } from '@dcl/mini-comms/dist/logic/handle-linear-protocol' 2 | import { PreviewComponents } from '../Preview' 3 | import { AboutResponse } from '@dcl/protocol/out-ts/decentraland/realm/about.gen' 4 | import { WebSocket } from 'ws' 5 | import { Router } from '@well-known-components/http-server' 6 | import { upgradeWebSocketResponse } from '@well-known-components/http-server/dist/ws' 7 | 8 | /** 9 | * This module handles the BFF mock and communications server for the preview mode. 10 | * It runs using @dcl/mini-comms implementing RFC-5 11 | */ 12 | 13 | export async function setupBffAndComms(components: PreviewComponents, router: Router) { 14 | router.get('/about', async (ctx) => { 15 | const host = ctx.url.host 16 | 17 | const body: AboutResponse = { 18 | acceptingUsers: true, 19 | bff: { healthy: false, publicUrl: '' }, 20 | comms: { 21 | healthy: true, 22 | protocol: 'v3', 23 | fixedAdapter: `ws-room:${ctx.url.protocol.replace(/^http/, 'ws')}//${host}/mini-comms/room-1` 24 | }, 25 | configurations: { 26 | realmName: 'LocalPreview', 27 | networkId: 1, 28 | globalScenesUrn: [], 29 | scenesUrn: [] 30 | }, 31 | content: { 32 | healthy: true, 33 | publicUrl: `${ctx.url.protocol}//${ctx.url.host}/content` 34 | }, 35 | lambdas: { 36 | healthy: true, 37 | publicUrl: `${ctx.url.protocol}//${ctx.url.host}/lambdas` 38 | }, 39 | healthy: true 40 | } 41 | 42 | return { body } 43 | }) 44 | 45 | router.get('/mini-comms/:roomId', async (ctx) => { 46 | return upgradeWebSocketResponse((ws: WebSocket) => { 47 | if (ws.protocol === 'rfc5' || ws.protocol === 'rfc4') { 48 | ws.on('error', (error) => { 49 | console.error(error) 50 | ws.close() 51 | }) 52 | 53 | ws.on('close', () => { 54 | console.debug('Websocket closed') 55 | }) 56 | 57 | handleSocketLinearProtocol(components, ws, ctx.params.roomId).catch((err) => { 58 | console.info(err) 59 | ws.close() 60 | }) 61 | } 62 | }) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/init/utils.ts: -------------------------------------------------------------------------------- 1 | import { sdk } from '@dcl/schemas' 2 | import inquirer, { QuestionCollection } from 'inquirer' 3 | import chalk from 'chalk' 4 | 5 | import { ErrorType, fail } from '../../utils/errors' 6 | import repositories from './repositories' 7 | import { sceneOptions } from './scene' 8 | import { InitOption } from './types' 9 | 10 | export function getProjectTypes() { 11 | return Object.values(sdk.ProjectType) 12 | .filter((a) => typeof a === 'string') 13 | .join(', ') 14 | } 15 | 16 | export async function getInitOption(type?: string): Promise { 17 | if (type) { 18 | if (!sdk.ProjectType.validate(type)) { 19 | fail( 20 | ErrorType.INIT_ERROR, 21 | `Invalid projectType: "${chalk.bold(type)}". Supported types are ${chalk.bold(getProjectTypes())}` 22 | ) 23 | } 24 | 25 | return { 26 | type: 'project', 27 | value: type as sdk.ProjectType 28 | } 29 | } 30 | 31 | const projectTypeList: QuestionCollection = [ 32 | { 33 | type: 'list', 34 | name: 'project', 35 | message: 'Choose a project type', 36 | choices: [ 37 | { 38 | name: 'Scene', 39 | value: 'scene-option' 40 | }, 41 | { 42 | name: 'Library', 43 | value: sdk.ProjectType.LIBRARY 44 | } 45 | ] 46 | } 47 | ] 48 | const answers = await inquirer.prompt(projectTypeList) 49 | 50 | if (answers.project === 'scene-option') { 51 | return sceneOptions() 52 | } 53 | 54 | if (sdk.ProjectType.validate(answers.project)) { 55 | return { 56 | type: 'project', 57 | value: answers.project 58 | } 59 | } 60 | 61 | fail(ErrorType.INIT_ERROR, `Couldn't get a valid first-level choice. Try to select a valid one.`) 62 | return {} as any 63 | } 64 | 65 | export function getRepositoryUrl(choice: InitOption): string | void { 66 | if (choice.value === sdk.ProjectType.SCENE) { 67 | return repositories.scenes[0].url 68 | } 69 | 70 | if (choice.value === sdk.ProjectType.LIBRARY) { 71 | return repositories.library 72 | } 73 | 74 | if (choice.type === 'scene') { 75 | return choice.value 76 | } 77 | } 78 | 79 | export function isValidTemplateUrl(url: string) { 80 | return /^https:\/\/github\.com\/decentraland(-scenes)?\/(.)+\.zip/.test(url) 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/content/ContentService.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { Entity, Scene } from '@dcl/schemas' 3 | import { createCatalystClient, CatalystClient, ContentClient } from 'dcl-catalyst-client' 4 | import { createFetchComponent } from '@well-known-components/fetch-component' 5 | 6 | import { Coords } from '../../utils/coordinateHelpers' 7 | import { fail, ErrorType } from '../../utils/errors' 8 | import { FileInfo } from '../Decentraland' 9 | 10 | export class ContentService extends EventEmitter { 11 | private readonly client: CatalystClient 12 | private contentClient?: ContentClient 13 | 14 | constructor(catalystServerUrl: string) { 15 | super() 16 | this.client = createCatalystClient({ 17 | url: catalystServerUrl, 18 | fetcher: createFetchComponent() 19 | }) 20 | } 21 | 22 | /** 23 | * Retrives the uploaded content information by a given Parcel (x y coordinates) 24 | * @param x 25 | * @param y 26 | */ 27 | async getParcelStatus(coordinates: Coords): Promise<{ cid: string; files: FileInfo[] }> { 28 | const entity = await this.fetchEntity(coordinates) 29 | const content = entity.content ? entity.content.map((entry) => ({ name: entry.file, cid: entry.hash })) : [] 30 | return { cid: entity.id, files: content } 31 | } 32 | 33 | /** 34 | * Retrives the content of the scene.json file from the content-server 35 | * @param x 36 | * @param y 37 | */ 38 | async getSceneData(coordinates: Coords): Promise { 39 | try { 40 | const entity = await this.fetchEntity(coordinates) 41 | return entity.metadata 42 | } catch (e) { 43 | throw e 44 | } 45 | } 46 | 47 | private async fetchEntity(coordinates: Coords): Promise { 48 | const pointer = `${coordinates.x},${coordinates.y}` 49 | try { 50 | if (!this.contentClient) { 51 | this.contentClient = await this.client.getContentClient() 52 | } 53 | const entities = await this.contentClient.fetchEntitiesByPointers([pointer]) 54 | const entity: Entity | undefined = entities[0] 55 | if (!entity) { 56 | fail(ErrorType.CONTENT_SERVER_ERROR, `Error retrieving parcel ${coordinates.x},${coordinates.y} information`) 57 | } 58 | return entity 59 | } catch (error: any) { 60 | fail(ErrorType.CONTENT_SERVER_ERROR, error.message) 61 | throw error 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/e2e/snapshots/help.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/e2e/help.test.ts` 2 | 3 | The actual snapshot is saved in `help.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## snapshot - dcl commands 8 | 9 | > Snapshot 1 10 | 11 | Set { 12 | 'init', 13 | 'build', 14 | 'start', 15 | 'deploy', 16 | 'deploy-deprecated', 17 | 'info', 18 | 'status', 19 | 'help', 20 | 'export', 21 | 'pack', 22 | 'install', 23 | 'coords', 24 | 'world-acl', 25 | 'workspace', 26 | } 27 | 28 | ## help command 29 | 30 | > Snapshot 1 31 | 32 | `␊ 33 | Decentraland CLI␊ 34 | ␊ 35 | Usage: dcl [command] [options]␊ 36 | ␊ 37 | Commands:␊ 38 | ␊ 39 | init Create a new Decentraland Scene project␊ 40 | build Build scene␊ 41 | start Start a local development server for a Decentraland Scene␊ 42 | install Sync decentraland libraries in bundleDependencies␊ 43 | install package Install a package␊ 44 | deploy Upload scene to a particular Decentraland's Content server␊ 45 | deploy-deprecated Upload scene to Decentraland's legacy content server (deprecated).␊ 46 | export Export scene to static website format (HTML, JS and CSS)␊ 47 | info [args] Displays information about a LAND, an Estate or an address␊ 48 | status [args] Displays the deployment status of the project or a given LAND␊ 49 | help [cmd] Displays complete help for given command␊ 50 | version Display current version of dcl␊ 51 | coords Set the parcels in your scene␊ 52 | world-acl [args] Manage DCL worlds permissions, dcl help world-acl for more information.␊ 53 | workspace subcommand Make a workspace level action, dcl help workspace for more information.␊ 54 | ␊ 55 | Options:␊ 56 | ␊ 57 | -h, --help Displays complete help for used command or subcommand␊ 58 | -v, --version Display current version of dcl␊ 59 | ␊ 60 | Example:␊ 61 | ␊ 62 | - Show complete help for the subcommand "deploy"␊ 63 | ␊ 64 | $ dcl help deploy␊ 65 | ␊ 66 | ` 67 | -------------------------------------------------------------------------------- /test/e2e/init.test.ts: -------------------------------------------------------------------------------- 1 | import test, { ExecutionContext } from 'ava' 2 | import { sdk } from '@dcl/schemas' 3 | 4 | import { help } from '../../src/commands/init' 5 | import pathsExistOnDir from '../../src/utils/pathsExistOnDir' 6 | import { createSandbox } from '../helpers/sandbox' 7 | import { runCommand, Response, endCommand } from '../helpers/commando' 8 | 9 | const initCommand = (dirPath: string, args?: string) => runCommand(dirPath, 'init', args) 10 | 11 | async function projectCreatedSuccessfully(t: ExecutionContext, dirPath: string, type: sdk.ProjectType) { 12 | const files = DEFAULT_FILES[type] 13 | const pathsExists = await pathsExistOnDir(dirPath, files) 14 | pathsExists.slice(0, files.length).forEach((file) => t.true(file)) 15 | } 16 | 17 | const DEFAULT_FILES: Record = { 18 | [sdk.ProjectType.SCENE]: [ 19 | 'src/game.ts', 20 | 'scene.json', 21 | 'package.json', 22 | 'node_modules', 23 | '.dclignore', 24 | 'node_modules/decentraland-ecs' 25 | ], 26 | [sdk.ProjectType.PORTABLE_EXPERIENCE]: ['wearable.json', 'AvatarWearables_TX.png', 'src/game.ts'], 27 | [sdk.ProjectType.SMART_ITEM]: ['scene.json', 'package.json', 'asset.json'], 28 | [sdk.ProjectType.LIBRARY]: [] 29 | } 30 | 31 | test('snapshot - dcl help init', (t) => { 32 | t.snapshot(help()) 33 | }) 34 | 35 | test('dcl init with prompt', async (t) => { 36 | await createSandbox(async (dirPath: string) => { 37 | const cmd = initCommand(dirPath) 38 | cmd.orderedWhen(/Choose a project type/, () => { 39 | console.log('Choose a project type') 40 | return [Response.ENTER] 41 | }) 42 | cmd.orderedWhen(/Choose a scene/, () => { 43 | console.log('Choose a scene') 44 | return [Response.ENTER] 45 | }) 46 | 47 | await endCommand(cmd) 48 | await projectCreatedSuccessfully(t, dirPath, sdk.ProjectType.SCENE) 49 | }) 50 | }) 51 | 52 | test('dcl init with -p option', async (t) => { 53 | await createSandbox(async (dirPath: string) => { 54 | const cmd = initCommand(dirPath, '-p scene') 55 | await endCommand(cmd) 56 | await projectCreatedSuccessfully(t, dirPath, sdk.ProjectType.SCENE) 57 | }) 58 | }) 59 | 60 | test('dcl init with invalid -p option', async (t) => { 61 | await createSandbox(async (dirPath: string) => { 62 | const cmd = initCommand(dirPath, '-p invalidoption') 63 | await endCommand(cmd) 64 | const [sceneJson] = await pathsExistOnDir(dirPath, ['scene.json']) 65 | t.false(sceneJson) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/commands/init/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | import { Decentraland } from '../../lib/Decentraland' 4 | import installDependencies from '../../project/installDependencies' 5 | import { Analytics } from '../../utils/analytics' 6 | import { downloadRepoZip } from '../../utils/download' 7 | import { fail, ErrorType } from '../../utils/errors' 8 | import { isEmptyDirectory } from '../../utils/filesystem' 9 | import * as spinner from '../../utils/spinner' 10 | import { getInitOption, getRepositoryUrl, isValidTemplateUrl } from './utils' 11 | import { args } from './help' 12 | import { InitOption } from './types' 13 | 14 | export { help } from './help' 15 | 16 | export async function main() { 17 | const dcl = new Decentraland({ workingDir: process.cwd() }) 18 | const project = dcl.workspace.getSingleProject() 19 | const isEmpty = await isEmptyDirectory(process.cwd()) 20 | if (!isEmpty) { 21 | fail(ErrorType.INIT_ERROR, `Project directory isn't empty`) 22 | return 23 | } 24 | 25 | if (!project) { 26 | fail(ErrorType.INIT_ERROR, 'Cannot init a project in workspace directory') 27 | return 28 | } 29 | const projectArg = args['--project'] 30 | const templateArg = args['--template'] 31 | 32 | let url: string | void 33 | let choice: InitOption | void 34 | if (templateArg && isValidTemplateUrl(templateArg)) { 35 | choice = { 36 | type: 'scene', 37 | value: templateArg 38 | } 39 | url = templateArg 40 | } else { 41 | choice = await getInitOption(projectArg) 42 | url = getRepositoryUrl(choice) 43 | } 44 | 45 | if (!url) { 46 | fail(ErrorType.INIT_ERROR, 'Cannot get a choice') 47 | return 48 | } 49 | 50 | const type = choice.type === 'scene' ? 'scene-template' : choice.value 51 | 52 | try { 53 | spinner.create('Downloading example...') 54 | 55 | await downloadRepoZip(url, project.getProjectWorkingDir()) 56 | 57 | spinner.succeed('Example downloaded') 58 | } catch (error: any) { 59 | spinner.fail(`Failed fetching the repo ${url}.`) 60 | fail(ErrorType.INIT_ERROR, error.message) 61 | } 62 | 63 | const skipInstall = args['--skip-install'] 64 | 65 | if (!skipInstall) { 66 | try { 67 | await installDependencies(dcl.getWorkingDir(), true) 68 | } catch (error: any) { 69 | fail(ErrorType.INIT_ERROR, error.message) 70 | } 71 | } 72 | 73 | console.log(chalk.green(`\nSuccess! Run 'dcl start' to see your scene\n`)) 74 | Analytics.sceneCreated({ projectType: type, url }) 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/nodeAndNpmVersion.ts: -------------------------------------------------------------------------------- 1 | import { getCLIPackageJson } from './moduleHelpers' 2 | import semver from 'semver' 3 | import { FileDescriptorStandardOption, runCommand } from './shellCommands' 4 | 5 | export const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm' 6 | 7 | export function getNodeVersion(): string { 8 | return process.versions.node 9 | } 10 | 11 | export async function getNpmVersion(): Promise { 12 | let npmVersion: string = '' 13 | function onOutData(data: any) { 14 | const arrStr = data.toString().split('.') 15 | if (arrStr.length === 3) { 16 | npmVersion = data.toString().split('\n')[0] 17 | } 18 | } 19 | await runCommand({ 20 | workingDir: process.cwd(), 21 | command: npm, 22 | args: ['-v'], 23 | fdStandards: FileDescriptorStandardOption.SEND_TO_CALLBACK, 24 | cb: { 25 | onOutData 26 | } 27 | }) 28 | 29 | return npmVersion 30 | } 31 | 32 | export async function checkNodeAndNpmVersion() { 33 | const requiredVersion = await getCLIPackageJson<{ 34 | userEngines: { 35 | minNodeVersion: string 36 | minNpmVersion: string 37 | } 38 | }>() 39 | 40 | try { 41 | const nodeVersion = await getNodeVersion() 42 | const npmVersion = await getNpmVersion() 43 | 44 | if (nodeVersion) { 45 | if (semver.lt(nodeVersion, requiredVersion.userEngines.minNodeVersion)) { 46 | console.error( 47 | `Decentraland CLI runs over node version ${requiredVersion.userEngines.minNodeVersion} or greater, current is ${nodeVersion}.` 48 | ) 49 | process.exit(1) 50 | } 51 | } else { 52 | console.error( 53 | `It's not possible to check node version, version ${requiredVersion.userEngines.minNodeVersion} or greater is required to run Decentraland CLI.` 54 | ) 55 | process.exit(1) 56 | } 57 | 58 | if (npmVersion) { 59 | if (semver.lt(npmVersion, requiredVersion.userEngines.minNpmVersion)) { 60 | console.warn( 61 | `⚠ Decentraland CLI works correctly installing packages with npm version ${requiredVersion.userEngines.minNpmVersion} or greater, current is ${npmVersion}.` 62 | ) 63 | } 64 | } else { 65 | console.warn( 66 | `⚠ It's not possible to check npm version, version ${requiredVersion.userEngines.minNpmVersion} or greater is required to Decentraland CLI works correctly.` 67 | ) 68 | } 69 | } catch (err) { 70 | console.warn(`⚠ It was not possible to check npm version or node.`, err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/adapters/proto/broker.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | 5 | enum MessageType { 6 | UNKNOWN_MESSAGE_TYPE = 0; 7 | WELCOME = 1; 8 | CONNECT = 2; 9 | WEBRTC_OFFER = 3; 10 | WEBRTC_ANSWER = 4; 11 | WEBRTC_ICE_CANDIDATE = 5; 12 | PING = 6; 13 | SUBSCRIPTION = 7; 14 | AUTH = 8; 15 | 16 | TOPIC = 9; 17 | TOPIC_FW = 10; 18 | 19 | TOPIC_IDENTITY = 11; 20 | TOPIC_IDENTITY_FW = 12; 21 | } 22 | 23 | enum Role { 24 | UNKNOWN_ROLE = 0; 25 | CLIENT = 1; 26 | COMMUNICATION_SERVER = 2; 27 | } 28 | 29 | enum Format { 30 | UNKNOWN_FORMAT = 0; 31 | PLAIN = 1; 32 | GZIP = 2; 33 | } 34 | 35 | // NOTE: coordination messsages 36 | 37 | message CoordinatorMessage { 38 | MessageType type = 1; 39 | } 40 | 41 | message WelcomeMessage { 42 | MessageType type = 1; 43 | uint64 alias = 2; 44 | repeated uint64 available_servers = 3; 45 | } 46 | 47 | message ConnectMessage { 48 | MessageType type = 1; 49 | uint64 from_alias = 2; 50 | uint64 to_alias = 3; 51 | } 52 | 53 | message WebRtcMessage { 54 | MessageType type = 1; 55 | uint64 from_alias = 2; 56 | uint64 to_alias = 3; 57 | bytes data = 4; 58 | } 59 | 60 | // NOTE: comm server messsages 61 | 62 | message MessageHeader { 63 | MessageType type = 1; 64 | } 65 | 66 | message PingMessage { 67 | MessageType type = 1; 68 | double time = 2; 69 | } 70 | 71 | // NOTE: topics is a space separated string in the format specified by Format 72 | message SubscriptionMessage { 73 | MessageType type = 1; 74 | Format format = 2; 75 | bytes topics = 3; 76 | } 77 | 78 | // NOTE: comm server messsages 79 | 80 | message AuthMessage { 81 | MessageType type = 1; 82 | Role role = 2; 83 | bytes body = 3; 84 | } 85 | 86 | message TopicMessage { 87 | MessageType type = 1; 88 | uint64 from_alias = 2; 89 | string topic = 3; 90 | bytes body = 4; 91 | } 92 | 93 | message TopicFWMessage { 94 | MessageType type = 1; 95 | uint64 from_alias = 2; 96 | bytes body = 3; 97 | } 98 | 99 | message TopicIdentityMessage { 100 | MessageType type = 1; 101 | uint64 from_alias = 2; 102 | string topic = 3; 103 | bytes identity = 4; 104 | Role role = 5; 105 | bytes body = 6; 106 | } 107 | 108 | message TopicIdentityFWMessage { 109 | MessageType type = 1; 110 | uint64 from_alias = 2; 111 | bytes identity = 3; 112 | Role role = 4; 113 | bytes body = 5; 114 | } 115 | -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import arg from 'arg' 3 | 4 | import { Decentraland } from '../lib/Decentraland' 5 | import { formatList } from '../utils/logging' 6 | import { Analytics } from '../utils/analytics' 7 | import { isValid, getObject } from '../utils/coordinateHelpers' 8 | import { fail, ErrorType } from '../utils/errors' 9 | import { parseTarget } from '../utils/land' 10 | 11 | export const help = () => ` 12 | Usage: ${chalk.bold('dcl status [target] [options]')} 13 | 14 | ${chalk.dim('Options:')} 15 | 16 | -h, --help Displays complete help 17 | -n, --network Choose between ${chalk.bold('mainnet')} and ${chalk.bold('sepolia')} (default 'mainnet') 18 | 19 | 20 | ${chalk.dim('Examples:')} 21 | 22 | - Get Decentraland Scene information of the current project" 23 | 24 | ${chalk.green('$ dcl status')} 25 | 26 | - Get Decentraland Scene information of the parcel ${chalk.bold('-12, 40')}" 27 | 28 | ${chalk.green('$ dcl status -12,40')} 29 | ` 30 | 31 | export async function main() { 32 | const args = arg( 33 | { 34 | '--help': Boolean, 35 | '--network': String, 36 | '-h': '--help', 37 | '-n': '--network' 38 | }, 39 | { permissive: true } 40 | ) 41 | 42 | const dcl = new Decentraland({ workingDir: process.cwd() }) 43 | 44 | const target = parseTarget(args._) 45 | let coords 46 | 47 | if (target) { 48 | if (!isValid(target)) { 49 | fail(ErrorType.STATUS_ERROR, `Invalid target "${chalk.bold(target)}"`) 50 | } 51 | 52 | coords = getObject(target) 53 | } 54 | 55 | const project = dcl.workspace.getSingleProject() 56 | if (!coords && project) { 57 | await project.validateExistingProject() 58 | coords = await project.getParcelCoordinates() 59 | } 60 | 61 | if (!coords) { 62 | fail(ErrorType.STATUS_ERROR, `Cannot get the coords`) 63 | return 64 | } else { 65 | const { cid, files } = await dcl.getParcelStatus(coords.x, coords.y) 66 | Analytics.statusCmd({ type: 'coordinates', target: coords }) 67 | logStatus(files, cid, `${coords.x},${coords.y}`) 68 | } 69 | } 70 | 71 | function logStatus(files: any[], cid: string, coords: string) { 72 | const serializedList = formatList(files, { spacing: 2, padding: 2 }) 73 | 74 | if (files.length === 0) { 75 | console.log(chalk.italic('\n No information available')) 76 | } else { 77 | console.log(`\n Deployment status for ${coords}:`) 78 | if (cid) { 79 | console.log(`\n Project CID: ${cid}`) 80 | } 81 | console.log(serializedList) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/project/projectInfo.ts: -------------------------------------------------------------------------------- 1 | import { sdk } from '@dcl/schemas' 2 | import { WearableJson } from '@dcl/schemas/dist/sdk' 3 | import fs, { readJsonSync } from 'fs-extra' 4 | import path from 'path' 5 | import { ASSET_JSON_FILE, WEARABLE_JSON_FILE } from '../utils/project' 6 | 7 | export type ProjectInfo = { 8 | sceneId: string 9 | sceneType: sdk.ProjectType 10 | } 11 | 12 | export function smartWearableNameToId(name: string) { 13 | return name.toLocaleLowerCase().replace(/ /g, '-') 14 | } 15 | 16 | export function getProjectInfo(workDir: string): ProjectInfo | null { 17 | const wearableJsonPath = path.resolve(workDir, WEARABLE_JSON_FILE) 18 | if (fs.existsSync(wearableJsonPath)) { 19 | try { 20 | const wearableJson = readJsonSync(wearableJsonPath) 21 | if (WearableJson.validate(wearableJson)) { 22 | return { 23 | sceneId: smartWearableNameToId(wearableJson.name), 24 | sceneType: sdk.ProjectType.PORTABLE_EXPERIENCE 25 | } 26 | } else { 27 | const errors = (WearableJson.validate.errors || []).map((a) => `${a.data} ${a.message}`).join('') 28 | 29 | if (errors.length > 0) { 30 | console.error(`Unable to validate '${WEARABLE_JSON_FILE}' properly, please check it: ${errors}`) 31 | } else { 32 | console.error(`Unable to validate '${WEARABLE_JSON_FILE}' properly, please check it.`) 33 | } 34 | return null 35 | } 36 | } catch (err) { 37 | console.error(`Unable to load ${WEARABLE_JSON_FILE} properly, please check it.`, err) 38 | return null 39 | } 40 | } 41 | 42 | const assetJsonPath = path.resolve(workDir, ASSET_JSON_FILE) 43 | if (fs.existsSync(assetJsonPath)) { 44 | // Validate, if is not valid, return null 45 | const assetJson = readJsonSync(assetJsonPath) 46 | if (assetJson.assetType) { 47 | const docUrl = 'https://docs.decentraland.org/development-guide/smart-wearables/' 48 | console.error(`Field assetType was used to discern smart wearable from smart item, but it's no longer support. 49 | Please if you're trying to develop a smart wearable read the docs, you probably need to change the 'asset.json' to 'wearable.json'. 50 | This 'wearable.json' has a different format that previous one. 51 | More information: ${docUrl}`) 52 | return null 53 | } 54 | 55 | return { 56 | sceneId: 'b64-' + Buffer.from(workDir).toString('base64'), 57 | sceneType: sdk.ProjectType.SMART_ITEM 58 | } 59 | } 60 | 61 | return { 62 | sceneId: 'b64-' + Buffer.from(workDir).toString('base64'), 63 | sceneType: sdk.ProjectType.SCENE 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/filesystem.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | 3 | /** 4 | * Checks if a folder exists and creates it if necessary. 5 | * @param path One or multiple paths to be checked. 6 | */ 7 | export async function ensureFolder(path: string | Array): Promise { 8 | if (typeof path === 'string') { 9 | if (await fs.pathExists(path)) { 10 | return 11 | } 12 | await fs.mkdir(path) 13 | } 14 | 15 | if (Array.isArray(path)) { 16 | if (path.length === 0) { 17 | return 18 | } else if (path.length === 1) { 19 | return ensureFolder(path[0]) 20 | } else { 21 | await ensureFolder(path[0]) 22 | await ensureFolder(path.slice(1)) 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * Merges the provided content with a json file 29 | * @param path The path to the subject json file 30 | * @param content The content to be applied (as a plain object) 31 | */ 32 | export async function writeJSON(path: string, content: any): Promise { 33 | let currentFile 34 | 35 | try { 36 | currentFile = await readJSON(path) 37 | } catch (e) { 38 | currentFile = {} 39 | } 40 | 41 | const strContent = JSON.stringify({ ...currentFile, ...content }, null, 2) 42 | 43 | return fs.outputFile(path, strContent) 44 | } 45 | 46 | /** 47 | * Reads a file and parses it's JSON content 48 | * @param path The path to the subject json file 49 | */ 50 | export async function readJSON(path: string): Promise { 51 | const content = await fs.readFile(path, 'utf-8') 52 | return JSON.parse(content) as T 53 | } 54 | 55 | /** 56 | * Reads a file and parses it's JSON content 57 | * @param path The path to the subject json file 58 | */ 59 | export function readJSONSync(path: string): T { 60 | const content = fs.readFileSync(path, 'utf-8') 61 | return JSON.parse(content) as T 62 | } 63 | 64 | /** 65 | * Returns true if the directory is empty 66 | */ 67 | export async function isEmptyDirectory(dir: string = '.'): Promise { 68 | const files = await fs.readdir(dir) 69 | return files.length === 0 70 | } 71 | 72 | /** 73 | * Returns th name of the Home directory in a platform-independent way. 74 | * @returns `USERPROFILE` or `HOME` 75 | */ 76 | export function getUserHome(): string { 77 | return process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'] || '' 78 | } 79 | 80 | export type PackageJson> = { 81 | name: string 82 | version: string 83 | main?: string 84 | dependencies?: Record 85 | devDependencies?: Record 86 | peerDependencies?: Record 87 | // https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bundleddependencies 88 | bundledDependencies?: string[] 89 | bundleDependencies?: string[] 90 | } & T 91 | -------------------------------------------------------------------------------- /src/utils/shellCommands.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | 3 | export enum FileDescriptorStandardOption { 4 | SILENT = 1, 5 | PIPE = 2, 6 | ONLY_IF_THROW = 3, 7 | SEND_TO_CALLBACK = 4 8 | } 9 | 10 | export type FDCallback = { 11 | onErrorData?: (data: string) => void 12 | onOutData?: (data: string) => void 13 | } 14 | 15 | export function runCommand({ 16 | workingDir, 17 | command, 18 | args, 19 | fdStandards, 20 | cb 21 | }: { 22 | workingDir: string 23 | command: string 24 | args: string[] 25 | fdStandards?: FileDescriptorStandardOption 26 | cb?: FDCallback 27 | }): Promise { 28 | const standardOption = fdStandards || FileDescriptorStandardOption.SILENT 29 | return new Promise((resolve, reject) => { 30 | const child = spawn(command, args, { 31 | shell: true, 32 | cwd: workingDir, 33 | env: { ...process.env, NODE_ENV: '' } 34 | }) 35 | 36 | let stdOut = '' 37 | let stdErr = '' 38 | 39 | if (standardOption === FileDescriptorStandardOption.PIPE) { 40 | child.stdout.pipe(process.stdout) 41 | child.stderr.pipe(process.stderr) 42 | } else if (standardOption === FileDescriptorStandardOption.ONLY_IF_THROW) { 43 | child.stdout.on('data', (data) => { 44 | stdOut += data.toString() 45 | }) 46 | 47 | child.stderr.on('data', (data) => { 48 | stdErr += data.toString() 49 | }) 50 | } else if (standardOption === FileDescriptorStandardOption.SEND_TO_CALLBACK) { 51 | child.stdout.on('data', (data) => { 52 | if (cb?.onOutData) { 53 | cb.onOutData(data.toString()) 54 | } 55 | }) 56 | 57 | child.stderr.on('data', (data) => { 58 | if (cb?.onErrorData) { 59 | cb.onErrorData(data.toString()) 60 | } 61 | }) 62 | } 63 | 64 | child.on('close', (code) => { 65 | const errorMessage = `Command '${command}' with args '${args.join(' ')}' exited with code ${code}. \n 66 | > Working directory: ${workingDir} ` 67 | 68 | if (code !== 0) { 69 | if (standardOption === FileDescriptorStandardOption.ONLY_IF_THROW) { 70 | reject( 71 | new Error(`${errorMessage} \n 72 | > Standard output: \n ${stdOut} \n 73 | > Error output: \n ${stdErr} \n`) 74 | ) 75 | } else { 76 | reject(new Error(errorMessage)) 77 | } 78 | } 79 | resolve() 80 | }) 81 | }) 82 | } 83 | 84 | export function downloadRepo(workingDir: string, url: string, destinationPath: string): Promise { 85 | return runCommand({ 86 | workingDir, 87 | command: 'git', 88 | args: ['clone', '--depth', '1', url, destinationPath], 89 | fdStandards: FileDescriptorStandardOption.ONLY_IF_THROW 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/workspace.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | import { Analytics } from '../utils/analytics' 4 | import { warning } from '../utils/logging' 5 | import { fail, ErrorType } from '../utils/errors' 6 | 7 | import { createWorkspace, initializeWorkspace } from '../lib/Workspace' 8 | 9 | export const help = () => ` 10 | Usage: ${chalk.bold('dcl workspace SUBCOMMAND [options]')} 11 | 12 | ${chalk.dim('Sub commands:')} 13 | 14 | init Create a workspace looking for subfolder Decentraland projects. 15 | ls List all projects in the current workspace 16 | add Add a project in the current workspace. 17 | 18 | ${chalk.dim('Options:')} 19 | 20 | -h, --help Displays complete help 21 | ` 22 | 23 | async function init() { 24 | try { 25 | await initializeWorkspace(process.cwd()) 26 | console.log(chalk.green(`\nSuccess! Run 'dcl start' to preview your workspace.\n`)) 27 | } catch (err: any) { 28 | fail(ErrorType.WORKSPACE_ERROR, err.message) 29 | } 30 | 31 | Analytics.sceneCreated({ projectType: 'workspace' }) 32 | } 33 | 34 | async function listProjects() { 35 | const workingDir = process.cwd() 36 | const workspace = createWorkspace({ workingDir }) 37 | 38 | if (workspace.isSingleProject()) { 39 | fail(ErrorType.WORKSPACE_ERROR, `There is no a workspace in the current directory.`) 40 | } 41 | 42 | console.log(`\nWorkspace in folder ${workingDir}`) 43 | for (const [index, project] of workspace.getAllProjects().entries()) { 44 | const projectPath = project.getProjectWorkingDir().replace(`${workingDir}\\`, '').replace(`${workingDir}/`, '') 45 | console.log(`> Project ${index + 1} in: ${projectPath}`) 46 | } 47 | console.log('') 48 | } 49 | 50 | async function addProject() { 51 | if (process.argv.length <= 4) { 52 | fail(ErrorType.WORKSPACE_ERROR, `Missing folder of new project.`) 53 | } 54 | 55 | const newProjectPath = process.argv[4] 56 | const workspace = createWorkspace({ workingDir: process.cwd() }) 57 | if (workspace.isSingleProject()) { 58 | fail(ErrorType.WORKSPACE_ERROR, `There is no a workspace in the current directory.`) 59 | } 60 | 61 | await workspace.addProject(newProjectPath) 62 | console.log(chalk.green(`\nSuccess! Run 'dcl start' to preview your workspace and see the new project added.\n`)) 63 | } 64 | 65 | export async function main() { 66 | if (process.argv.length <= 3) { 67 | fail(ErrorType.WORKSPACE_ERROR, `The subcommand is not recognized`) 68 | } 69 | 70 | const subcommandList: Record Promise> = { 71 | init, 72 | ls: listProjects, 73 | help: async () => console.log(help()), 74 | add: addProject 75 | } 76 | const subcommand = process.argv[3].toLowerCase() 77 | 78 | warning(`(Beta)`) 79 | 80 | if (subcommand in subcommandList) { 81 | await subcommandList[subcommand]() 82 | } else { 83 | fail(ErrorType.WORKSPACE_ERROR, `The subcommand ${subcommand} is not recognized`) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decentraland CLI ![Decentraland Logo](css/logo.svg) 2 | 3 | [![CI](https://github.com/decentraland/cli/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/decentraland/cli/actions/workflows/ci.yml) 4 | [![chat on Discord](https://img.shields.io/discord/417796904760639509.svg?logo=discord)](https://dcl.gg/discord) 5 | 6 | This CLI provides tooling/commands to assist you in the [scenes](https://github.com/decentraland-scenes/Awesome-Repository) development process. Some of the commands will help you scaffold a new scene project, locally start and visualize the scene in order to test it and deploy it to a [content server](https://github.com/decentraland/catalyst/tree/master/content) to be incorporated in your Decentraland parcel. 7 | 8 | ## Usage 9 | 10 | To install the latest version of `dcl` (Decentraland CLI), run this command: 11 | 12 | ```bash 13 | npm install -g decentraland 14 | ``` 15 | 16 | To learn what you can do with the CLI run the following command: 17 | 18 | ```bash 19 | dcl --help 20 | ``` 21 | 22 | See more details at [Decentraland docs](https://docs.decentraland.org/creator/development-guide/cli/). 23 | 24 | ## Documentation 25 | 26 | For details on how to use Decentraland developer tools, check our [documentation site](https://docs.decentraland.org) 27 | 28 | ## Contributing 29 | 30 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 31 | 2. Install dependencies with `npm install`. 32 | 3. Build the project by running `npm run build`. 33 | 4. Link the CLI with: `npm link`. The `dcl` command should now be available. 34 | 5. You can run tests with `npm test` 35 | 36 | **NOTE:** you can set the environment variable `DEBUG=true` to see all debugging info 37 | 38 | ## Releasing 39 | Just update the version on the `package.json` file and merge to master. 40 | 41 | ## Configuration 42 | 43 | `dcl` can be configured in several ways to adapt it to another environment other than the default one. To do this you have to either set environment variables or change your `~/.dclinfo` file: 44 | 45 | | Variable name | Enviroment variable | `~/.dclinfo` | 46 | | ------------------------ | :-----------------: | :------------: | 47 | | Provider | RPC_URL | - | 48 | | MANA Token Contract | MANA_TOKEN | MANAToken | 49 | | LAND Registry Contract | LAND_REGISTRY | LANDRegistry | 50 | | Estate Registry Contract | ESTATE_REGISTRY | EstateRegistry | 51 | | Content Server URL | CONTENT_URL | contentUrl | 52 | | Segment API key | SEGMENT_KEY | segmentKey | 53 | | Track Analytics data | TRACK_STATS | trackStats | 54 | 55 | ## Copyright info 56 | This repository is protected with a standard Apache 2 license. See the terms and conditions in the [LICENSE](https://github.com/decentraland/cli/blob/master/LICENSE) file. 57 | -------------------------------------------------------------------------------- /src/commands/pack.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import chalk from 'chalk' 4 | import { sdk } from '@dcl/schemas' 5 | 6 | import * as spinner from '../utils/spinner' 7 | import { packProject } from '../lib/smartItems/packProject' 8 | import { buildSmartItem } from '../lib/smartItems/buildSmartItem' 9 | import getProjectFilePaths from '../utils/getProjectFilePaths' 10 | import { buildTypescript } from '../utils/moduleHelpers' 11 | import { fail } from 'assert' 12 | import { createWorkspace } from '../lib/Workspace' 13 | 14 | export const help = () => ` 15 | Usage: ${chalk.bold('dcl pack [options]')} 16 | 17 | ${chalk.dim('There are no available options yet (experimental feature)')} 18 | 19 | ${chalk.dim('Example:')} 20 | 21 | - Pack your project into a .zip file: 22 | 23 | ${chalk.green('$ dcl pack')} 24 | ` 25 | 26 | export async function main(): Promise { 27 | const workingDir = process.cwd() 28 | const workspace = createWorkspace({ workingDir }) 29 | const project = workspace.getSingleProject() 30 | 31 | if (project === null) { 32 | fail("You should't use `dcl pack` in a workspace folder.") 33 | } 34 | 35 | const projectInfo = project.getInfo() 36 | 37 | const zipFileName = 38 | projectInfo.sceneType === sdk.ProjectType.PORTABLE_EXPERIENCE ? 'portable-experience.zip' : 'item.zip' 39 | 40 | try { 41 | if (projectInfo.sceneType === sdk.ProjectType.SMART_ITEM) { 42 | await buildSmartItem(workingDir) 43 | } else if (projectInfo.sceneType === sdk.ProjectType.PORTABLE_EXPERIENCE) { 44 | await buildTypescript({ 45 | workingDir, 46 | watch: false, 47 | production: true 48 | }) 49 | } 50 | } catch (error) { 51 | console.error('Could not build the project properly, please check errors.', error) 52 | } 53 | 54 | spinner.create('Packing project') 55 | 56 | const ignoreFileContent = await fs.readFile(path.resolve(workingDir, '.dclignore'), 'utf-8') 57 | const filePaths = await getProjectFilePaths(workingDir, ignoreFileContent) 58 | 59 | let totalSize = 0 60 | for (const filePath of filePaths) { 61 | const stat = fs.statSync(filePath) 62 | if (stat.isFile()) { 63 | totalSize += stat.size 64 | } 65 | } 66 | 67 | if (projectInfo.sceneType === sdk.ProjectType.PORTABLE_EXPERIENCE) { 68 | const MAX_WEARABLE_SIZE = 2097152 69 | const MAX_WEARABLE_SIZE_MB = Math.round(MAX_WEARABLE_SIZE / 1024 / 1024) 70 | if (totalSize > MAX_WEARABLE_SIZE) { 71 | console.error(`The sumatory of all packed files exceed the limit of wearable size (${MAX_WEARABLE_SIZE_MB}MB - ${MAX_WEARABLE_SIZE} bytes). 72 | Please try to remove unneccessary files and/or reduce the files size, you can ignore file adding in .dclignore.`) 73 | } 74 | } 75 | 76 | const packDir = path.resolve(workingDir, zipFileName) 77 | await fs.remove(packDir) 78 | await packProject(filePaths, packDir) 79 | 80 | spinner.succeed( 81 | `Pack successful. Total size: ${Math.round((totalSize * 100) / 1024 / 1024) / 100}MB - ${totalSize} bytes` 82 | ) 83 | return 0 84 | } 85 | -------------------------------------------------------------------------------- /src/lib/WorldsContentServer.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import events from 'wildcards' 3 | import { ErrorType, fail } from '../utils/errors' 4 | import { DCLInfo, getConfig } from '../config' 5 | import { LinkerResponse } from './LinkerAPI' 6 | import { ethSign, recoverAddressFromEthSignature } from '@dcl/crypto/dist/crypto' 7 | import { IdentityType } from '@dcl/crypto' 8 | import { hexToBytes } from 'eth-connect' 9 | import { WorldsContentServerLinkerAPI } from './WorldsContentServerLinkerAPI' 10 | import { EthAddress } from '@dcl/schemas' 11 | 12 | export type WorldsContentServerArguments = { 13 | worldName: string 14 | allowed: EthAddress[] 15 | oldAllowed: EthAddress[] 16 | targetContent: string 17 | linkerPort?: number 18 | isHttps?: boolean 19 | config?: DCLInfo 20 | method: 'put' | 'delete' 21 | } 22 | 23 | export class WorldsContentServer extends EventEmitter { 24 | options: WorldsContentServerArguments 25 | targetContent: string 26 | environmentIdentity?: IdentityType 27 | 28 | constructor(args: WorldsContentServerArguments) { 29 | super() 30 | this.options = args 31 | this.options.config = this.options.config || getConfig() 32 | this.targetContent = args.targetContent 33 | if (process.env.DCL_PRIVATE_KEY) { 34 | this.createWallet(process.env.DCL_PRIVATE_KEY) 35 | } 36 | } 37 | 38 | async link(payload: string): Promise { 39 | return new Promise(async (resolve, reject) => { 40 | const linker = new WorldsContentServerLinkerAPI({ 41 | worldName: this.options.worldName, 42 | allowed: this.options.allowed, 43 | oldAllowed: this.options.oldAllowed, 44 | targetContent: this.options.targetContent, 45 | method: this.options.method, 46 | expiration: 120, 47 | payload 48 | }) 49 | events(linker, '*', this.pipeEvents.bind(this)) 50 | linker.on('link:success', async (message: LinkerResponse) => { 51 | resolve(message) 52 | }) 53 | 54 | try { 55 | await linker.link(this.options.linkerPort!, !!this.options.isHttps) 56 | } catch (e) { 57 | reject(e) 58 | } 59 | }) 60 | } 61 | 62 | async getAddressAndSignature(messageToSign: string): Promise { 63 | if (this.environmentIdentity) { 64 | return { 65 | signature: ethSign(hexToBytes(this.environmentIdentity.privateKey), messageToSign), 66 | address: this.environmentIdentity.address 67 | } 68 | } 69 | return this.link(messageToSign) 70 | } 71 | 72 | private pipeEvents(event: string, ...args: any[]) { 73 | this.emit(event, ...args) 74 | } 75 | 76 | private createWallet(privateKey: string) { 77 | let length = 64 78 | 79 | if (privateKey.startsWith('0x')) { 80 | length = 66 81 | } 82 | 83 | if (privateKey.length !== length) { 84 | fail(ErrorType.DEPLOY_ERROR, 'Addresses should be 64 characters length.') 85 | } 86 | 87 | const pk = hexToBytes(privateKey) 88 | const msg = Math.random().toString() 89 | const signature = ethSign(pk, msg) 90 | const address = recoverAddressFromEthSignature(signature, msg) 91 | this.environmentIdentity = { 92 | address, 93 | privateKey, 94 | publicKey: '0x' 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | release: 8 | types: 9 | - created 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | platform: [macos-latest, ubuntu-latest, windows-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 16.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | cache: npm 23 | - name: install 24 | run: npm ci 25 | - name: build 26 | run: npm run build 27 | - name: test:win 28 | if: matrix.platform == 'windows-latest' 29 | run: npm run test:win 30 | - name: test 31 | if: matrix.platform != 'windows-latest' 32 | run: npm run test:ci 33 | - name: lint 34 | run: npm run lint 35 | 36 | publish: 37 | runs-on: ubuntu-latest 38 | needs: [build] 39 | outputs: 40 | cli_s3_bucket_key: ${{ steps.publish_cli.outputs.s3-bucket-key }} 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Use Node.js 16.x 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: 16.x 47 | cache: npm 48 | - name: install 49 | run: npm ci 50 | - name: build 51 | run: npm run build 52 | - name: Publish 53 | id: publish_cli 54 | uses: menduz/oddish-action@master 55 | with: 56 | registry-url: "https://registry.npmjs.org" 57 | access: public 58 | 59 | ## publish every package to s3 60 | s3-bucket: ${{ secrets.SDK_TEAM_S3_BUCKET }} 61 | s3-bucket-key-prefix: 'decentraland-cli/branch/${{ github.head_ref || github.ref }}' 62 | s3-bucket-region: ${{ secrets.SDK_TEAM_AWS_REGION }} 63 | env: 64 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 65 | AWS_DEFAULT_REGION: us-east-1 66 | AWS_ACCESS_KEY_ID: ${{ secrets.SDK_TEAM_AWS_ID }} 67 | AWS_SECRET_ACCESS_KEY: ${{ secrets.SDK_TEAM_AWS_SECRET }} 68 | 69 | notify_deployment: 70 | needs: [publish] 71 | if: ${{ github.event.pull_request.number }} 72 | runs-on: ubuntu-latest 73 | name: Deployment Notification 74 | steps: 75 | - name: Find Comment 76 | uses: peter-evans/find-comment@v3 77 | id: fc 78 | with: 79 | issue-number: ${{ github.event.pull_request.number }} 80 | comment-author: 'github-actions[bot]' 81 | body-includes: Test this pull request 82 | - name: Generate S3 URL 83 | id: url-generator 84 | run: echo "body=${{ secrets.SDK_TEAM_S3_BASE_URL }}/${{ needs.publish.outputs.cli_s3_bucket_key }}" >> $GITHUB_OUTPUT 85 | - name: Create or update comment 86 | uses: peter-evans/create-or-update-comment@v4 87 | with: 88 | comment-id: ${{ steps.fc.outputs.comment-id }} 89 | issue-number: ${{ github.event.pull_request.number }} 90 | body: | 91 | # Test this pull request 92 | - The `cli` package can be tested by globally install 93 | ```bash 94 | npm i -g "${{ steps.url-generator.outputs.body }}" 95 | ``` 96 | edit-mode: replace 97 | -------------------------------------------------------------------------------- /test/unit/sceneJson/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from '@dcl/schemas' 2 | import test from 'ava' 3 | import sinon from 'sinon' 4 | 5 | import * as spinner from '../../../src/utils/spinner' 6 | import { validateScene } from '../../../src/sceneJson/utils' 7 | import sceneJson from '../resources/data/scene.json' 8 | 9 | const sandbox = sinon.createSandbox() 10 | type SinnonKeys = keyof typeof spinner 11 | let mockSpinner: Record 12 | 13 | test.before(() => { 14 | mockSpinner = { 15 | create: sandbox.stub(spinner, 'create').callsFake((a) => a), 16 | fail: sandbox.stub(spinner, 'fail').callsFake((a) => a), 17 | warn: sandbox.stub(spinner, 'warn').callsFake((a) => a), 18 | succeed: sandbox.stub(spinner, 'succeed').callsFake((a) => a), 19 | info: sandbox.stub(spinner, 'info').callsFake((a) => a) 20 | } 21 | }) 22 | test.after(() => { 23 | sandbox.restore() 24 | }) 25 | 26 | test('Unit - validateScene should fail with default metadata values', (t) => { 27 | sandbox.reset() 28 | const scene: Scene = sceneJson 29 | 30 | t.false(validateScene(scene, true)) 31 | 32 | sandbox.assert.calledOnce(mockSpinner.create) 33 | sandbox.assert.calledOnce(mockSpinner.warn) 34 | sandbox.assert.notCalled(mockSpinner.fail) 35 | sandbox.assert.notCalled(mockSpinner.succeed) 36 | }) 37 | 38 | test('Unit - validateScene should pass with valid scene.json', (t) => { 39 | sandbox.reset() 40 | const scene: Scene = { 41 | ...sceneJson, 42 | display: { 43 | navmapThumbnail: 'navmap-thumbnail.png', 44 | description: 'some-description', 45 | title: 'some-title' 46 | } 47 | } 48 | 49 | t.true(validateScene(scene, true)) 50 | 51 | sandbox.assert.calledOnce(mockSpinner.create) 52 | sandbox.assert.calledOnce(mockSpinner.succeed) 53 | sandbox.assert.notCalled(mockSpinner.fail) 54 | sandbox.assert.notCalled(mockSpinner.warn) 55 | }) 56 | 57 | test('Unit - validateScene should fail with missing main prop', (t) => { 58 | sandbox.reset() 59 | const scene: Scene = { 60 | ...sceneJson, 61 | main: undefined 62 | } 63 | 64 | t.false(validateScene(scene, true)) 65 | 66 | sandbox.assert.calledOnce(mockSpinner.create) 67 | sandbox.assert.calledOnce(mockSpinner.fail) 68 | sandbox.assert.notCalled(mockSpinner.succeed) 69 | sandbox.assert.notCalled(mockSpinner.warn) 70 | }) 71 | 72 | test('Unit - validateScene should fail with invalid description prop', (t) => { 73 | sandbox.reset() 74 | const scene: Scene = { 75 | ...sceneJson, 76 | display: { 77 | description: 1 as any as string, 78 | title: 'asd', 79 | navmapThumbnail: 'asd' 80 | } 81 | } 82 | 83 | t.false(validateScene(scene, true)) 84 | 85 | sandbox.assert.calledOnce(mockSpinner.create) 86 | sandbox.assert.calledOnce(mockSpinner.fail) 87 | sandbox.assert.notCalled(mockSpinner.succeed) 88 | sandbox.assert.notCalled(mockSpinner.warn) 89 | }) 90 | 91 | test('Unit - validateScene should not called spinner with log in false', (t) => { 92 | sandbox.reset() 93 | const scene: Scene = sceneJson 94 | 95 | t.false(validateScene(scene, false)) 96 | 97 | sandbox.assert.notCalled(mockSpinner.create) 98 | sandbox.assert.notCalled(mockSpinner.fail) 99 | sandbox.assert.notCalled(mockSpinner.succeed) 100 | sandbox.assert.notCalled(mockSpinner.warn) 101 | }) 102 | -------------------------------------------------------------------------------- /src/utils/logging.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | import { isDebug } from './env' 4 | import { isRecord } from '.' 5 | 6 | export function debug(...messages: any[]): void { 7 | if (isDebug()) { 8 | console.log(...messages) 9 | } 10 | } 11 | 12 | export function error(message: string): string { 13 | return `${chalk.red('Error:')} ${message}` 14 | } 15 | 16 | export function warning(message: string): string { 17 | return `${chalk.yellow('Warning: ')}${message}` 18 | } 19 | 20 | export function tabulate(spaces: number = 0) { 21 | return spaces > 0 ? ' '.repeat(spaces) : '' 22 | } 23 | 24 | export function isEmpty(obj: Record) { 25 | if (!obj) return true 26 | const keys = Object.keys(obj) 27 | if (!keys.length) { 28 | return true 29 | } 30 | return keys.every(($) => obj[$] === undefined || obj[$] === [] || obj[$] === {} || obj[$] === '') 31 | } 32 | 33 | export function formatDictionary( 34 | obj: Record, 35 | options: { spacing: number; padding: number }, 36 | level: number = 1, 37 | context?: 'array' | 'object' 38 | ): string { 39 | let buf = '' 40 | const keys = obj ? Object.keys(obj) : [] 41 | 42 | keys.forEach((key, i) => { 43 | const item = obj[key] 44 | 45 | const separator = context === 'array' && i === 0 ? '' : tabulate(options.spacing * level + options.padding) 46 | 47 | if (Array.isArray(item)) { 48 | buf = buf.concat(separator, `${chalk.bold(key)}: `, formatList(item, options, level + 1, 'object'), '\n') 49 | } else if (isRecord(item)) { 50 | const isHidden = isEmpty(item) 51 | const content = isHidden 52 | ? `: ${chalk.italic('No information available')}\n` 53 | : `:\n${formatDictionary(item, options, level + 1, 'object')}` 54 | buf = buf.concat(separator, `${chalk.bold(key)}`, content) 55 | } else if (item) { 56 | buf = buf.concat(separator, `${chalk.bold(key)}: `, JSON.stringify(item), '\n') 57 | } 58 | }) 59 | 60 | return buf 61 | } 62 | 63 | export function formatList( 64 | list: Array, 65 | options: { spacing: number; padding: number }, 66 | level: number = 1, 67 | _context?: 'array' | 'object' 68 | ): string { 69 | let buf = '' 70 | const separator = '\n' + tabulate(options.spacing * level + options.padding) + '- ' 71 | if (list.length) { 72 | buf = list.reduce((buf, item, _i) => { 73 | if (Array.isArray(item)) { 74 | return buf.concat(separator, formatList(list, options, level + 1, 'array')) 75 | } else if (typeof item === 'object') { 76 | return buf.concat(separator, formatDictionary(item, options, level + 1, 'array')) 77 | } else if (item !== undefined) { 78 | return buf.concat(separator, JSON.stringify(item)) 79 | } else { 80 | return buf 81 | } 82 | }, '') 83 | } else { 84 | buf = chalk.italic('No information available') 85 | } 86 | 87 | return buf 88 | } 89 | 90 | export function formatOutdatedMessage(arg: { 91 | package: string 92 | installedVersion: string 93 | latestVersion: string 94 | }): string { 95 | return [ 96 | `A package is outdated:`, 97 | ` ${arg.package}:`, 98 | ` installed: ${arg.installedVersion}`, 99 | ` latest: ${arg.latestVersion}`, 100 | ` to upgrade to the latest version run the command:`, 101 | ` npm install ${arg.package}@latest` 102 | ].join('\n') 103 | } 104 | -------------------------------------------------------------------------------- /src/commands/coords.ts: -------------------------------------------------------------------------------- 1 | import { Scene, sdk } from '@dcl/schemas' 2 | import chalk from 'chalk' 3 | 4 | import { fail, ErrorType } from '../utils/errors' 5 | import { getObject, isValid, getString, Coords } from '../utils/coordinateHelpers' 6 | import { getSceneFile, setSceneFile } from '../sceneJson' 7 | import * as spinner from '../utils/spinner' 8 | import { createWorkspace } from '../lib/Workspace' 9 | 10 | export function help() { 11 | return ` 12 | Usage: ${chalk.bold('dcl coords [parcels]')} 13 | 14 | ${chalk.dim('Options:')} 15 | 16 | -h, --help Displays complete help 17 | 18 | ${chalk.dim('Examples:')} 19 | - ${chalk.bold('Single parcel')} 20 | - Pass a single argument with the scene coords. This coordinate is also set as the base parcel. 21 | ${chalk.green('$ dcl coords 0,0')} 22 | 23 | - ${chalk.bold('Multiple parcels')} 24 | - Pass two arguments: the South-West and the North-East parcels. The South-West parcel is also set as the base parcel. 25 | ${chalk.green('$ dcl coords 0,0 1,1')} 26 | 27 | - ${chalk.bold('Customize Base Parcel')} 28 | - Pass three arguments: the South-West and the North-East parcels, and the parcel to use as a base parcel. 29 | ${chalk.green('$ dcl coords 0,0 1,1 1,0')} 30 | ` 31 | } 32 | 33 | export async function main() { 34 | spinner.create('Generating coords') 35 | 36 | const parcels = process.argv.slice(process.argv.findIndex((arg) => arg === 'coords') + 1) 37 | const workingDir = process.cwd() 38 | 39 | const workspace = createWorkspace({ workingDir }) 40 | const project = workspace.getSingleProject() 41 | if (project === null) { 42 | fail(ErrorType.INFO_ERROR, `Can not change a coords of workspace.`) 43 | } else if (project.getInfo().sceneType !== sdk.ProjectType.SCENE) { 44 | fail(ErrorType.INFO_ERROR, 'Only parcel scenes can be edited the coords property.') 45 | } 46 | 47 | if (!parcels || !parcels.length) { 48 | fail(ErrorType.INFO_ERROR, 'Please provide a target to retrieve data') 49 | } 50 | 51 | if (parcels.length > 3) { 52 | fail(ErrorType.INFO_ERROR, 'Invalid number of args') 53 | } 54 | 55 | const invalidParcel = parcels.find((p) => !isValid(p)) 56 | if (invalidParcel) { 57 | fail(ErrorType.INFO_ERROR, `Invalid target "${chalk.bold(invalidParcel)}"`) 58 | } 59 | 60 | const parcelObjects = parcels.map(getObject) 61 | const { scene, ...sceneJson } = await getSceneFile(workingDir) 62 | const newScene = getSceneObject(parcelObjects) 63 | const parsedSceneJson: Scene = { 64 | ...sceneJson, 65 | scene: newScene 66 | } 67 | 68 | await setSceneFile(parsedSceneJson, workingDir) 69 | spinner.succeed() 70 | } 71 | 72 | function getSceneObject([sw, ne, baseParcel = sw]: Coords[]): Scene['scene'] { 73 | if (!ne) { 74 | const coords = getString(sw) 75 | return { base: coords, parcels: [coords] } 76 | } 77 | 78 | const getValues = (key: keyof Coords) => 79 | Array.from({ 80 | length: ne[key] - sw[key] + 1 81 | }).map((_, value) => value + sw[key]) 82 | 83 | const xValues = getValues('x') 84 | const yValues = getValues('y') 85 | const parcels = xValues.reduce((acc: string[], x) => { 86 | const coord = yValues.map((y) => getString({ x, y })) 87 | return acc.concat(coord) 88 | }, []) 89 | const base = parcels.length ? getString(baseParcel) : '' 90 | if (!parcels.includes(base)) { 91 | spinner.fail() 92 | fail(ErrorType.INFO_ERROR, `Invalid base parcel ${chalk.bold(base)}`) 93 | } 94 | 95 | return { parcels, base } 96 | } 97 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | import chalk from 'chalk' 3 | 4 | import { fail } from 'assert' 5 | import { Decentraland } from '../lib/Decentraland' 6 | import installDependencies from '../project/installDependencies' 7 | import updateBundleDependenciesField from '../project/updateBundleDependenciesField' 8 | import { Analytics } from '../utils/analytics' 9 | import { buildTypescript, isOnline } from '../utils/moduleHelpers' 10 | 11 | export const help = () => ` 12 | Usage: ${chalk.bold('dcl build [options]')} 13 | 14 | ${chalk.dim('Options:')} 15 | 16 | -h, --help Displays complete help 17 | -w, --watch Watch for file changes and build on change 18 | -p, --production Build without sourcemaps 19 | --skip-version-checks Skip the ECS and CLI version checks, avoid the warning message and launch anyway 20 | --skip-install Skip installing dependencies 21 | 22 | ${chalk.dim('Example:')} 23 | 24 | - Build your scene: 25 | 26 | ${chalk.green('$ dcl build')} 27 | ` 28 | 29 | export async function main(): Promise { 30 | const args = arg({ 31 | '--help': Boolean, 32 | '-h': '--help', 33 | '--watch': Boolean, 34 | '-w': '--watch', 35 | '--skip-version-checks': Boolean, 36 | '--production': Boolean, 37 | '-p': '--production', 38 | '--skip-install': Boolean 39 | }) 40 | 41 | const dcl = new Decentraland({ 42 | watch: args['--watch'] || args['-w'] || false, 43 | workingDir: process.cwd() 44 | }) 45 | 46 | const skipVersionCheck = args['--skip-version-checks'] 47 | const skipInstall = args['--skip-install'] 48 | const online = await isOnline() 49 | const errors = [] 50 | 51 | for (const project of dcl.workspace.getAllProjects()) { 52 | const needDependencies = await project.needsDependencies() 53 | 54 | if (needDependencies && !skipInstall) { 55 | if (online) { 56 | await installDependencies(project.getProjectWorkingDir(), false /* silent */) 57 | } else { 58 | fail('This project can not start as you are offline and dependencies need to be installed.') 59 | } 60 | } 61 | 62 | if (!skipVersionCheck) { 63 | await project.checkCLIandECSCompatibility() 64 | } 65 | 66 | try { 67 | await updateBundleDependenciesField({ 68 | workDir: project.getProjectWorkingDir() 69 | }) 70 | } catch (err) { 71 | console.warn(`Unable to update bundle dependencies field.`, err) 72 | } 73 | 74 | if (await project.isTypescriptProject()) { 75 | try { 76 | await buildTypescript({ 77 | workingDir: project.getProjectWorkingDir(), 78 | watch: !!args['--watch'], 79 | production: !!args['--production'] 80 | }) 81 | } catch (err) { 82 | errors.push({ project, err }) 83 | } 84 | } 85 | } 86 | 87 | if (errors.length) { 88 | const projectList = errors.map((item) => item.project.getProjectWorkingDir()).join('\n\t') 89 | 90 | throw new Error(`Error compiling (see logs above) the scenes: \n\t${projectList}`) 91 | } 92 | 93 | if (dcl.workspace.isSingleProject()) { 94 | const baseCoords = await dcl.workspace.getBaseCoords() 95 | Analytics.buildScene({ 96 | projectHash: dcl.getProjectHash(), 97 | ecs: await dcl.workspace.getSingleProject()!.getEcsPackageVersion(), 98 | coords: baseCoords, 99 | isWorkspace: false 100 | }) 101 | } else { 102 | Analytics.buildScene({ 103 | projectHash: dcl.getProjectHash(), 104 | isWorkspace: true 105 | }) 106 | } 107 | 108 | return 0 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decentraland", 3 | "version": "3.18.0", 4 | "description": "Decentraland CLI developer tool.", 5 | "bin": { 6 | "dcl": "dist/index.js" 7 | }, 8 | "files": [ 9 | "dist", 10 | "samples", 11 | "abi" 12 | ], 13 | "scripts": { 14 | "start": "npm run watch", 15 | "watch": "tsc -p tsconfig.json -w", 16 | "build": "tsc -p tsconfig.json && chmod +x dist/index.js", 17 | "lint": "eslint . --ext .ts", 18 | "lint:fix": "eslint --fix . --ext .ts", 19 | "test": "FORCE_COLOR=1 ava -T 5m --verbose", 20 | "test:dry": "FORCE_COLOR=1 ava -T 5m --update-snapshots", 21 | "test:ci": "FORCE_COLOR=1 ava -v --fail-fast -T 5m", 22 | "test:win": "set FORCE_COLOR=1 ava -v --fail-fast -T 5m " 23 | }, 24 | "repository": "decentraland/cli", 25 | "keywords": [ 26 | "decentraland", 27 | "cli", 28 | "dcl", 29 | "mana", 30 | "land" 31 | ], 32 | "ava": { 33 | "extensions": [ 34 | "ts" 35 | ], 36 | "require": [ 37 | "ts-node/register" 38 | ] 39 | }, 40 | "license": "Apache-2.0", 41 | "homepage": "https://github.com/decentraland/cli", 42 | "devDependencies": { 43 | "@dcl/eslint-config": "^2.0.0", 44 | "@types/analytics-node": "^3.1.8", 45 | "@types/archiver": "^5.3.1", 46 | "@types/chalk": "^2.2.0", 47 | "@types/cors": "^2.8.12", 48 | "@types/express": "^4.17.13", 49 | "@types/fs-extra": "^9.0.13", 50 | "@types/glob": "^7.2.0", 51 | "@types/inquirer": "^8.2.1", 52 | "@types/node": "^17.0.27", 53 | "@types/node-fetch": "^2.6.1", 54 | "@types/puppeteer": "^1.11.2", 55 | "@types/semver": "^7.3.9", 56 | "@types/sinon": "^5.0.5", 57 | "@types/uuid": "^8.3.4", 58 | "@types/ws": "^8.5.3", 59 | "ava": "^4.2.0", 60 | "husky": "^7.0.4", 61 | "puppeteer": "^1.17.0", 62 | "sinon": "^7.1.1", 63 | "ts-node": "^4.1.0" 64 | }, 65 | "dependencies": { 66 | "@dcl/crypto": "^3.4.5", 67 | "@dcl/ecs-scene-utils": "^1.7.5", 68 | "@dcl/linker-dapp": "^0.13.0", 69 | "@dcl/mini-comms": "1.0.0", 70 | "@dcl/protocol": "^1.0.0-9254639032.commit-05cd554", 71 | "@dcl/schemas": "^11.9.1", 72 | "@well-known-components/env-config-provider": "^1.1.2-20220801195549.commit-101c273", 73 | "@well-known-components/http-server": "^1.1.6-20220927190058.commit-2dfb235", 74 | "@well-known-components/logger": "^3.0.0", 75 | "@well-known-components/metrics": "^2.0.1-20220909150423.commit-8f7e5bc", 76 | "analytics-node": "^6.0.0", 77 | "archiver": "^5.3.1", 78 | "arg": "^5.0.1", 79 | "body-parser": "^1.20.0", 80 | "chalk": "^4.1.2", 81 | "chokidar": "^3.5.3", 82 | "dcl-catalyst-client": "^21.7.0", 83 | "dcl-catalyst-commons": "^9.0.1", 84 | "dcl-node-runtime": "^1.0.0", 85 | "eth-connect": "^6.0.3", 86 | "extract-zip": "^2.0.1", 87 | "fp-future": "^1.0.1", 88 | "fs-extra": "^10.1.0", 89 | "glob": "^8.0.1", 90 | "google-protobuf": "^3.21.2", 91 | "ignore": "^4.0.6", 92 | "inquirer": "^8.2.2", 93 | "isomorphic-fetch": "^3.0.0", 94 | "node-fetch": "^2.6.7", 95 | "opn": "^6.0.0", 96 | "ora": "^5.4.1", 97 | "package-json": "^7.0.0", 98 | "portfinder": "^1.0.28", 99 | "semver": "^7.3.7", 100 | "typescript": "^4.6.3", 101 | "uuid": "^8.3.2", 102 | "wildcards": "^1.0.2", 103 | "ws": "^8.5.0" 104 | }, 105 | "engines": { 106 | "node": ">=16.0.0", 107 | "npm": ">=8.0.0", 108 | "yarn": "please use npm" 109 | }, 110 | "userEngines": { 111 | "minNodeVersion": "14.0.0", 112 | "minNpmVersion": "6.0.0" 113 | }, 114 | "prettier": { 115 | "semi": false, 116 | "singleQuote": true, 117 | "trailingComma": "none" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/e2e/coords.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs-extra' 3 | import test from 'ava' 4 | import { Scene } from '@dcl/schemas' 5 | 6 | import { help } from '../../src/commands/coords' 7 | import sandbox from '../helpers/sandbox' 8 | import Commando from '../helpers/commando' 9 | import { isDebug } from '../../src/utils/env' 10 | import { readJSON } from '../../src/utils/filesystem' 11 | 12 | function coordsCommand(dirPath: string, coords: string[]) { 13 | return new Promise((resolve) => { 14 | const cmd = new Commando(`node ${path.resolve('dist', 'index.js')} coords ${coords.join(' ')}`, { 15 | silent: !isDebug(), 16 | workingDir: dirPath, 17 | env: { NODE_ENV: 'development' } 18 | }) 19 | 20 | cmd.on('end', async () => { 21 | resolve() 22 | }) 23 | }) 24 | } 25 | 26 | test('snapshot - dcl help instal', (t) => { 27 | t.snapshot(help()) 28 | }) 29 | 30 | test('coords 0,8', async (t) => { 31 | await sandbox(async (dirPath, done) => { 32 | const sw = '0,8' 33 | const scenePath = path.resolve(dirPath, 'scene.json') 34 | await fs.writeJson(scenePath, { scene: {} }) 35 | await coordsCommand(dirPath, [sw]) 36 | 37 | const sceneJson = await readJSON(scenePath) 38 | const expectedScene: Scene['scene'] = { 39 | base: '0,8', 40 | parcels: ['0,8'] 41 | } 42 | t.deepEqual(sceneJson.scene, expectedScene) 43 | done() 44 | }) 45 | }) 46 | 47 | test('coords 0,0 2,3', async (t) => { 48 | await sandbox(async (dirPath, done) => { 49 | const sw = '0,0' 50 | const ne = '2,3' 51 | const scenePath = path.resolve(dirPath, 'scene.json') 52 | await fs.writeJson(scenePath, { scene: {} }) 53 | await coordsCommand(dirPath, [sw, ne]) 54 | 55 | const sceneJson = await readJSON(scenePath) 56 | const expectedScene: Scene['scene'] = { 57 | base: '0,0', 58 | parcels: ['0,0', '0,1', '0,2', '0,3', '1,0', '1,1', '1,2', '1,3', '2,0', '2,1', '2,2', '2,3'] 59 | } 60 | t.deepEqual(sceneJson.scene, expectedScene) 61 | done() 62 | }) 63 | }) 64 | 65 | test('coords 0,0 2,3 2,2', async (t) => { 66 | await sandbox(async (dirPath, done) => { 67 | const sw = '0,0' 68 | const ne = '2,3' 69 | const base = '2,2' 70 | const scenePath = path.resolve(dirPath, 'scene.json') 71 | await fs.writeJson(scenePath, { scene: {} }) 72 | await coordsCommand(dirPath, [sw, ne, base]) 73 | 74 | const sceneJson = await readJSON(scenePath) 75 | const expectedScene: Scene['scene'] = { 76 | base: '2,2', 77 | parcels: ['0,0', '0,1', '0,2', '0,3', '1,0', '1,1', '1,2', '1,3', '2,0', '2,1', '2,2', '2,3'] 78 | } 79 | t.deepEqual(sceneJson.scene, expectedScene) 80 | done() 81 | }) 82 | }) 83 | 84 | test('coords 0,0 2,3 5,2 should fail with invalid base parcel', async (t) => { 85 | await sandbox(async (dirPath, done) => { 86 | const sw = '0,0' 87 | const ne = '2,3' 88 | const base = '5,2' 89 | const scenePath = path.resolve(dirPath, 'scene.json') 90 | await fs.writeJson(scenePath, { scene: {} }) 91 | await coordsCommand(dirPath, [sw, ne, base]) 92 | const sceneJson = await readJSON(scenePath) 93 | const expectedScene = {} 94 | 95 | t.deepEqual(sceneJson.scene, expectedScene) 96 | done() 97 | }) 98 | }) 99 | 100 | test('coords 2,3 0,0 should fail with invalid sw ne', async (t) => { 101 | await sandbox(async (dirPath, done) => { 102 | const sw = '2,3' 103 | const ne = '0,0' 104 | const scenePath = path.resolve(dirPath, 'scene.json') 105 | await fs.writeJson(scenePath, { scene: {} }) 106 | await coordsCommand(dirPath, [sw, ne]) 107 | const sceneJson = await readJSON(scenePath) 108 | const expectedScene = {} 109 | 110 | t.deepEqual(sceneJson.scene, expectedScene) 111 | done() 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/e2e/start.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import test, { ExecutionContext } from 'ava' 3 | import fetch, { RequestInfo, RequestInit } from 'node-fetch' 4 | 5 | import * as start from '../../src/commands/start' 6 | import { isDebug } from '../../src/utils/env' 7 | import pathsExistOnDir from '../../src/utils/pathsExistOnDir' 8 | import Commando from '../helpers/commando' 9 | import sandbox from '../helpers/sandbox' 10 | import initProject from '../helpers/initProject' 11 | import { AboutResponse } from '@dcl/protocol/out-ts/bff/http-endpoints.gen' 12 | 13 | test('snapshot - dcl help start', (t) => { 14 | t.snapshot(start.help()) 15 | }) 16 | 17 | function startProject(dirPath): Promise { 18 | return new Promise((resolve) => { 19 | const command = new Commando( 20 | `node ${path.resolve('dist', 'index.js')} start --skip-version-checks --no-browser -p 8001`, 21 | { 22 | silent: !isDebug(), 23 | workingDir: dirPath, 24 | env: { NODE_ENV: 'development' } 25 | } 26 | ).when(/to exit/, async () => { 27 | resolve(command) 28 | }) 29 | }) 30 | } 31 | 32 | test('E2E - init && start command', async (t) => { 33 | await sandbox(async (dirPath, done) => { 34 | // We init project without installing dependencies so we test 35 | // that `dcl start` automatically install dependencies as well 36 | await initProject(dirPath, false) 37 | const startCmd = await startProject(dirPath) 38 | const response = await fetch(`http://localhost:8001`) 39 | const body = await response.text() 40 | t.snapshot(body) 41 | const [gameCompiledExists, nodeModulesExists, ecsModuleExists] = await pathsExistOnDir(dirPath, [ 42 | 'bin/game.js', 43 | 'node_modules', 44 | 'node_modules/decentraland-ecs' 45 | ]) 46 | 47 | t.true(gameCompiledExists) 48 | t.true(nodeModulesExists) 49 | t.true(ecsModuleExists) 50 | 51 | await testWearablePreview(t) 52 | await testAbout(t) 53 | 54 | startCmd.end() 55 | done() 56 | }) 57 | }) 58 | 59 | async function testWearablePreview(t: ExecutionContext) { 60 | const scene = await fetchJson('http://localhost:8001/scene.json') 61 | t.deepEqual( 62 | { display: scene.display }, 63 | { 64 | display: { 65 | title: 'DCL Scene', 66 | description: 'My new Decentraland project', 67 | navmapThumbnail: 'images/scene-thumbnail.png', 68 | favicon: 'favicon_asset' 69 | } 70 | }, 71 | 'get /scene.json works' 72 | ) 73 | } 74 | 75 | async function testAbout(t: ExecutionContext) { 76 | { 77 | const about = (await fetchJson('http://localhost:8001/about')) as AboutResponse 78 | t.is(about.content.publicUrl, 'http://localhost:8001/content', 'content server URL properly configured') 79 | t.is(about.lambdas.publicUrl, 'http://localhost:8001/lambdas', 'lambdas server URL properly configured') 80 | t.is( 81 | about.comms.fixedAdapter, 82 | 'ws-room:ws://localhost:8001/mini-comms/room-1', 83 | 'lambdas server URL properly configured' 84 | ) 85 | } 86 | { 87 | const about = (await fetchJson('http://127.0.0.1:8001/about', { 88 | headers: { 'x-forwarded-proto': 'https' } 89 | })) as AboutResponse 90 | t.is(about.content.publicUrl, 'https://127.0.0.1:8001/content', 'content server URL properly configured') 91 | t.is(about.lambdas.publicUrl, 'https://127.0.0.1:8001/lambdas', 'lambdas server URL properly configured') 92 | t.is( 93 | about.comms.fixedAdapter, 94 | 'ws-room:wss://127.0.0.1:8001/mini-comms/room-1', 95 | 'lambdas server URL properly configured' 96 | ) 97 | } 98 | } 99 | 100 | async function fetchJson(init: RequestInfo, param?: RequestInit) { 101 | const res = await fetch(init, param) 102 | if (!res.ok) throw new Error('Error fetching ' + JSON.stringify(init)) 103 | return res.json() 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/controllers/legacy-comms-v1.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@well-known-components/http-server' 2 | import WebSocket from 'ws' 3 | import { PreviewComponents } from '../Preview' 4 | import proto from '../adapters/proto/broker' 5 | 6 | // Handles Comms V1 7 | export function setupCommsV1(_components: PreviewComponents, _router: Router) { 8 | const connections = new Set() 9 | const topicsPerConnection = new WeakMap>() 10 | let connectionCounter = 0 11 | 12 | const aliasToUserId = new Map() 13 | 14 | function getTopicList(socket: WebSocket): Set { 15 | let set = topicsPerConnection.get(socket) 16 | if (!set) { 17 | set = new Set() 18 | topicsPerConnection.set(socket, set) 19 | } 20 | return set 21 | } 22 | 23 | return { 24 | adoptWebSocket(ws: WebSocket, userId: string) { 25 | const alias = ++connectionCounter 26 | 27 | aliasToUserId.set(alias, userId) 28 | 29 | console.log('Acquiring comms connection.') 30 | 31 | connections.add(ws) 32 | 33 | ws.on('message', (message) => { 34 | const data = message as Buffer 35 | const msgType = proto.CoordinatorMessage.deserializeBinary(data).getType() 36 | 37 | if (msgType === proto.MessageType.PING) { 38 | ws.send(data) 39 | } else if (msgType === proto.MessageType.TOPIC) { 40 | const topicMessage = proto.TopicMessage.deserializeBinary(data) 41 | 42 | const topic = topicMessage.getTopic() 43 | 44 | const topicFwMessage = new proto.TopicFWMessage() 45 | topicFwMessage.setType(proto.MessageType.TOPIC_FW) 46 | topicFwMessage.setFromAlias(alias) 47 | topicFwMessage.setBody(topicMessage.getBody_asU8()) 48 | 49 | const topicData = topicFwMessage.serializeBinary() 50 | 51 | // Reliable/unreliable data 52 | connections.forEach(($) => { 53 | if (ws !== $) { 54 | if (getTopicList($).has(topic)) { 55 | $.send(topicData) 56 | } 57 | } 58 | }) 59 | } else if (msgType === proto.MessageType.TOPIC_IDENTITY) { 60 | const topicMessage = proto.TopicIdentityMessage.deserializeBinary(data) 61 | 62 | const topic = topicMessage.getTopic() 63 | 64 | const topicFwMessage = new proto.TopicIdentityFWMessage() 65 | topicFwMessage.setType(proto.MessageType.TOPIC_IDENTITY_FW) 66 | topicFwMessage.setFromAlias(alias) 67 | topicFwMessage.setIdentity(aliasToUserId.get(alias)!) 68 | topicFwMessage.setRole(proto.Role.CLIENT) 69 | topicFwMessage.setBody(topicMessage.getBody_asU8()) 70 | 71 | const topicData = topicFwMessage.serializeBinary() 72 | 73 | // Reliable/unreliable data 74 | connections.forEach(($) => { 75 | if (ws !== $) { 76 | if (getTopicList($).has(topic)) { 77 | $.send(topicData) 78 | } 79 | } 80 | }) 81 | } else if (msgType === proto.MessageType.SUBSCRIPTION) { 82 | const topicMessage = proto.SubscriptionMessage.deserializeBinary(data) 83 | const rawTopics = topicMessage.getTopics() 84 | const topics = Buffer.from(rawTopics as string).toString('utf8') 85 | const set = getTopicList(ws) 86 | 87 | set.clear() 88 | topics.split(/\s+/g).forEach(($) => set.add($)) 89 | } 90 | }) 91 | 92 | ws.on('close', () => connections.delete(ws)) 93 | 94 | setTimeout(() => { 95 | const welcome = new proto.WelcomeMessage() 96 | welcome.setType(proto.MessageType.WELCOME) 97 | welcome.setAlias(alias) 98 | const data = welcome.serializeBinary() 99 | 100 | ws.send(data, (err) => { 101 | if (err) { 102 | try { 103 | ws.close() 104 | } catch {} 105 | } 106 | }) 107 | }, 100) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/lib/WorldsContentServerLinkerAPI.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import https from 'https' 3 | import { EventEmitter } from 'events' 4 | import fs from 'fs-extra' 5 | import express, { Express, NextFunction, Request, Response } from 'express' 6 | import cors from 'cors' 7 | import bodyParser from 'body-parser' 8 | import portfinder from 'portfinder' 9 | 10 | export type WorldsContentServerResponse = { 11 | address: string 12 | signature: string 13 | } 14 | 15 | type Route = ( 16 | path: string, 17 | fn: ( 18 | req: Request, 19 | resp?: Response, 20 | next?: NextFunction 21 | ) => Promise | unknown[]> | void | Record | unknown[] 22 | ) => void 23 | 24 | type Async = 'async' 25 | type Method = 'get' | 'post' 26 | type AsyncMethod = `${Async}${Capitalize}` 27 | 28 | type AsyncExpress = Express & { 29 | [key in AsyncMethod]: Route 30 | } 31 | 32 | /** 33 | * Events emitted by this class: 34 | * 35 | * link:ready - The server is up and running 36 | * link:success - Signature success 37 | * link:error - The transaction failed and the server was closed 38 | */ 39 | export class WorldsContentServerLinkerAPI extends EventEmitter { 40 | private app: AsyncExpress = express() as AsyncExpress 41 | 42 | constructor(private data: any) { 43 | super() 44 | } 45 | 46 | link(port: number, isHttps: boolean) { 47 | return new Promise(async (_, reject) => { 48 | let resolvedPort = port 49 | 50 | if (!resolvedPort) { 51 | try { 52 | resolvedPort = await portfinder.getPortPromise() 53 | } catch (e) { 54 | resolvedPort = 4044 55 | } 56 | } 57 | const protocol = isHttps ? 'https' : 'http' 58 | const url = `${protocol}://localhost:${resolvedPort}` 59 | 60 | this.setRoutes() 61 | 62 | this.on('link:error', (err) => { 63 | reject(err) 64 | }) 65 | 66 | const serverHandler = () => this.emit('link:ready', { url }) 67 | const eventHandler = () => (e: any) => { 68 | if (e.errno === 'EADDRINUSE') { 69 | reject(new Error(`Port ${resolvedPort} is already in use by another process`)) 70 | } else { 71 | reject(new Error(`Failed to start Linker App: ${e.message}`)) 72 | } 73 | } 74 | 75 | if (isHttps) { 76 | const privateKey = await fs.readFile(path.resolve(__dirname, '../../certs/localhost.key'), 'utf-8') 77 | const certificate = await fs.readFile(path.resolve(__dirname, '../../certs/localhost.crt'), 'utf-8') 78 | const credentials = { key: privateKey, cert: certificate } 79 | 80 | const httpsServer = https.createServer(credentials, this.app) 81 | httpsServer.listen(resolvedPort, serverHandler).on('error', eventHandler) 82 | } else { 83 | this.app.listen(resolvedPort, serverHandler).on('error', eventHandler) 84 | } 85 | }) 86 | } 87 | 88 | private setRoutes() { 89 | const linkerDapp = path.dirname(require.resolve('@dcl/linker-dapp/package.json')) 90 | this.app.use(cors()) 91 | this.app.use(express.static(linkerDapp)) 92 | this.app.use('/acl', express.static(linkerDapp)) 93 | this.app.use(bodyParser.json()) 94 | 95 | /** 96 | * Async method to try/catch errors 97 | */ 98 | const methods: Capitalize[] = ['Get', 'Post'] 99 | for (const method of methods) { 100 | const asyncMethod: AsyncMethod = `async${method}` 101 | this.app[asyncMethod] = async (path, fn) => { 102 | const originalMethod = method.toLocaleLowerCase() as Method 103 | this.app[originalMethod](path, async (req, res) => { 104 | try { 105 | const resp = await fn(req, res) 106 | res.send(resp || {}) 107 | } catch (e) { 108 | console.log(e) 109 | res.send(e) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | this.app.asyncGet('/api/acl', async () => { 116 | return await this.data 117 | }) 118 | 119 | this.app.asyncPost('/api/acl', (req) => { 120 | type Body = { 121 | address: string 122 | signature: string 123 | } 124 | const value = req.body as Body 125 | 126 | if (!value.address || !value.signature) { 127 | throw new Error(`Invalid payload: ${Object.keys(value).join(' - ')}`) 128 | } 129 | 130 | this.emit('link:success', value) 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/utils/coordinateHelpers.ts: -------------------------------------------------------------------------------- 1 | export interface IBounds { 2 | minX: number 3 | minY: number 4 | maxX: number 5 | maxY: number 6 | } 7 | 8 | export type Coords = { 9 | x: number 10 | y: number 11 | } 12 | 13 | /** 14 | * Returns metaverse coordinates bounds. 15 | */ 16 | export function getBounds(): IBounds { 17 | return { 18 | minX: -150, 19 | minY: -150, 20 | maxX: 165, 21 | maxY: 165 22 | } 23 | } 24 | 25 | /** 26 | * Parses a string-based set of coordinates. 27 | * - All spaces are removed 28 | * - Leading zeroes are removed 29 | * - `-0` is converted to `0` 30 | * @param coordinates An string containing coordinates in the `x,y; x,y; ...` format 31 | */ 32 | export function parse(coordinates: string): string[] { 33 | return coordinates.split(';').map((coord: string) => { 34 | const [x = 0, y = 0] = coord.split(',').map(($) => { 35 | return parseInt($, 10) 36 | .toString() // removes spaces :) 37 | .replace('-0', '0') 38 | .replace(/undefined|NaN/g, '0') 39 | }) 40 | return `${x},${y}` 41 | }) 42 | } 43 | 44 | /** 45 | * Returns a promise that resolves `true` if the given set of coordinates is valid. 46 | * For invalid coordinates, the promise will reject with an error message. 47 | * *This is meant to be used as an inquirer validator.* 48 | * 49 | * Empty inputs will resolve `true` 50 | * @param answers An string containing coordinates in the `x,y; x,y; ...` format 51 | */ 52 | export function validate(answers: string): Promise { 53 | return new Promise((resolve, reject) => { 54 | if (answers.trim().length === 0) { 55 | resolve(true) 56 | } else { 57 | answers.split(/;\s/g).forEach((answer) => { 58 | if (!isValid(answer)) { 59 | reject(new Error(`Invalid coordinate ${answer}`)) 60 | } 61 | }) 62 | resolve(true) 63 | } 64 | }) 65 | } 66 | 67 | /** 68 | * Returns true if the given coordinate's format is valid 69 | * 70 | * ``` 71 | * isValid('0,0') // returns true 72 | * isValid(', 0') // returns false 73 | * ``` 74 | * @param val The coodinate string 75 | */ 76 | export function isValid(val: string): boolean { 77 | if (!val.match(/^(-?\d)+\,(-?\d)+$/g)) { 78 | return false 79 | } 80 | return true 81 | } 82 | 83 | /** 84 | * Converts a string-based set of coordinates to an object 85 | * @param coords A string containing a set of coordinates 86 | */ 87 | export function getObject(coords: string): Coords 88 | /** 89 | * Converts a array-based set of coordinates to an object 90 | * @param coords An array containing a set of coordinates 91 | */ 92 | export function getObject(coords: number[]): Coords 93 | export function getObject(coords: number[] | string): Coords { 94 | const [x, y] = typeof coords === 'string' ? parse(coords)[0].split(',') : coords 95 | return { x: parseInt(x.toString(), 10), y: parseInt(y.toString(), 10) } 96 | } 97 | 98 | /** 99 | * Converts a Coords object to a string-based set of coordinates 100 | */ 101 | export function getString({ x, y }: Coords): string { 102 | return `${x},${y}` 103 | } 104 | 105 | /** 106 | * Returns true if the given coordinates are in metaverse bounds 107 | */ 108 | export function inBounds(x: number, y: number): boolean { 109 | const { minX, minY, maxX, maxY } = getBounds() 110 | return x >= minX && x <= maxX && y >= minY && y <= maxY 111 | } 112 | 113 | /** 114 | * Returns true if the given parcels array are connected 115 | */ 116 | export function areConnected(parcels: Coords[]): boolean { 117 | if (parcels.length === 0) { 118 | return false 119 | } 120 | const visited = visitParcel(parcels[0], parcels) 121 | return visited.length === parcels.length 122 | } 123 | 124 | function visitParcel(parcel: Coords, allParcels: Coords[] = [parcel], visited: Coords[] = []): Coords[] { 125 | const isVisited = visited.some((visitedParcel) => isEqual(visitedParcel, parcel)) 126 | if (!isVisited) { 127 | visited.push(parcel) 128 | const neighbours = getNeighbours(parcel.x, parcel.y, allParcels) 129 | neighbours.forEach((neighbours) => visitParcel(neighbours, allParcels, visited)) 130 | } 131 | return visited 132 | } 133 | 134 | function getIsNeighbourMatcher(x: number, y: number) { 135 | return (coords: Coords) => 136 | (coords.x === x && (coords.y + 1 === y || coords.y - 1 === y)) || 137 | (coords.y === y && (coords.x + 1 === x || coords.x - 1 === x)) 138 | } 139 | 140 | function getNeighbours(x: number, y: number, parcels: Coords[]): Coords[] { 141 | return parcels.filter(getIsNeighbourMatcher(x, y)) 142 | } 143 | 144 | export function isEqual(p1: Coords, p2: Coords): boolean { 145 | return p1.x === p2.x && p1.y === p2.y 146 | } 147 | -------------------------------------------------------------------------------- /test/helpers/commando.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { ChildProcess, spawn } from 'child_process' 3 | import { EventEmitter } from 'events' 4 | 5 | export interface IOptions { 6 | silent: boolean 7 | cmdPath?: string 8 | workingDir?: string 9 | env?: { [key: string]: string } 10 | } 11 | 12 | export interface IMatcherOptions { 13 | matchMany: boolean 14 | } 15 | 16 | export enum Response { 17 | YES = 'Yes\n', 18 | NO = 'no\n', 19 | DOWN = '\x1B\x5B\x42', 20 | UP = '\x1B\x5B\x41', 21 | ENTER = '\x0D', 22 | SPACE = '\x20' 23 | } 24 | 25 | class Commando extends EventEmitter { 26 | private proc: ChildProcess 27 | private matchers: { 28 | pattern: RegExp 29 | response: (mag: string) => any 30 | options: IMatcherOptions 31 | }[] = [] 32 | private promises: Promise[] = [] 33 | private onDataFn: (string) => void 34 | private orderedCommands: string[] = [] 35 | 36 | constructor(command: string, opts: IOptions = { silent: false, env: {} }, onDataFn?: (string) => void) { 37 | super() 38 | this.onDataFn = onDataFn 39 | const parts = command.split(' ') 40 | const cmd = opts.cmdPath ? path.resolve(opts.cmdPath, parts[0]) : parts[0] 41 | 42 | this.proc = spawn(cmd, parts.slice(1), { 43 | env: { ...process.env, ...opts.env }, 44 | cwd: opts.workingDir 45 | }) 46 | 47 | if (!opts.silent) { 48 | this.proc.stdout.pipe(process.stdout) 49 | } 50 | 51 | this.proc.stdout.on('data', (data) => this.onData(data.toString())) 52 | this.proc.stderr.on('data', (data) => this.emit('err', data.toString())) 53 | this.proc.on('close', async () => { 54 | void Promise.all(this.promises) 55 | this.emit('end') 56 | }) 57 | } 58 | 59 | when(pattern: string | RegExp, response: (msg: string) => any, options: IMatcherOptions = { matchMany: false }) { 60 | this.matchers.push({ pattern: new RegExp(pattern), response, options }) 61 | return this 62 | } 63 | 64 | orderedWhen( 65 | pattern: string | RegExp, 66 | response: (msg: string) => any, 67 | options: IMatcherOptions = { matchMany: false } 68 | ) { 69 | this.orderedCommands.push(pattern.toString()) 70 | this.when(pattern, response, options) 71 | return this 72 | } 73 | 74 | end() { 75 | this.proc.kill() 76 | } 77 | 78 | endWhen(pattern: string | RegExp, response?: (msg: string) => any, options: IMatcherOptions = { matchMany: false }) { 79 | const cb = (msg) => { 80 | if (response) { 81 | const res = response(msg) 82 | if ('then' in response || response.constructor.name === 'AsyncFunction') { 83 | this.promises.push(res) 84 | } 85 | } 86 | void Promise.all(this.promises).then(() => { 87 | this.proc.kill() 88 | }) 89 | } 90 | this.matchers.push({ pattern: new RegExp(pattern), response: cb, options }) 91 | return this 92 | } 93 | 94 | nextCommand() { 95 | const nextCommand = this.orderedCommands[0] 96 | this.orderedCommands = this.orderedCommands.slice(1) 97 | 98 | return nextCommand 99 | } 100 | 101 | private onData(data: string) { 102 | if (this.onDataFn) { 103 | this.onDataFn(data) 104 | } 105 | 106 | if (this.orderedCommands.length) { 107 | const match = this.matchers.find((m) => data.match(m.pattern)) 108 | if (match && this.nextCommand() !== match.pattern.toString()) { 109 | this.end() 110 | } 111 | } 112 | 113 | this.matchers.forEach((match, i) => { 114 | if (!data.match(match.pattern)) { 115 | return 116 | } 117 | 118 | const res = match.response(data) 119 | 120 | if ('then' in match.response) { 121 | this.promises.push(res) 122 | return 123 | } 124 | 125 | if (res && typeof res === 'string') { 126 | this.proc.stdin.write(res) 127 | } 128 | 129 | if (res && Array.isArray(res)) { 130 | res.forEach((r) => this.proc.stdin.write(r)) 131 | } 132 | 133 | if (!match.options.matchMany) { 134 | this.matchers.splice(i, 1) 135 | } 136 | }) 137 | } 138 | } 139 | 140 | export default Commando 141 | 142 | export function runCommand(dirPath: string, cmdName: string, args?: string) { 143 | const command = `node ${path.resolve('dist', 'index.js')} ${cmdName} ${args}` 144 | const cmd = new Commando(command, { 145 | silent: false, 146 | workingDir: dirPath, 147 | env: { NODE_ENV: 'development' } 148 | }) 149 | 150 | cmd.when(/Send anonymous usage stats to Decentraland?/, () => Response.NO) 151 | 152 | return cmd 153 | } 154 | 155 | export function endCommand(cmd: Commando) { 156 | return new Promise((resolve) => { 157 | cmd.on('end', resolve) 158 | }) 159 | } 160 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { readJSON, writeJSON, getUserHome } from './utils/filesystem' 4 | import { removeEmptyKeys } from './utils' 5 | import { isStableVersion } from './utils/moduleHelpers' 6 | import { isDevelopment } from './utils/env' 7 | 8 | export type DCLInfo = { 9 | fileExists?: boolean 10 | userId: string 11 | trackStats: boolean 12 | provider?: string 13 | MANAToken?: string 14 | LANDRegistry?: string 15 | EstateRegistry?: string 16 | catalystUrl?: string 17 | dclApiUrl?: string 18 | segmentKey?: string 19 | } 20 | 21 | let networkFlag: string = 'mainnet' 22 | let config: DCLInfo 23 | 24 | /** 25 | * Returns the path to the `.dclinfo` file located in the local HOME folder 26 | */ 27 | function getDCLInfoPath(): string { 28 | return path.resolve(getUserHome(), '.dclinfo') 29 | } 30 | 31 | /** 32 | * Reads the contents of the `.dclinfo` file 33 | */ 34 | async function readDCLInfo(): Promise { 35 | const filePath = getDCLInfoPath() 36 | try { 37 | const file = await readJSON(filePath) 38 | return file 39 | } catch (e) { 40 | return null 41 | } 42 | } 43 | 44 | /** 45 | * Creates the `.dclinfo` file in the HOME directory 46 | */ 47 | export function createDCLInfo(dclInfo: DCLInfo) { 48 | config = dclInfo 49 | return writeJSON(getDCLInfoPath(), dclInfo) 50 | } 51 | 52 | /** 53 | * Add new configuration to `.dclinfo` file 54 | */ 55 | export async function writeDCLInfo(newInfo: DCLInfo) { 56 | return writeJSON(getDCLInfoPath(), { ...config, newInfo }) 57 | } 58 | 59 | /** 60 | * Reads `.dclinfo` file and loads it in-memory to be sync-obtained with `getDCLInfo()` function 61 | */ 62 | export async function loadConfig(network: string): Promise { 63 | networkFlag = network 64 | config = (await readDCLInfo())! 65 | return config 66 | } 67 | 68 | /** 69 | * Returns the contents of the `.dclinfo` file. It needs to be loaded first with `loadConfig()` function 70 | */ 71 | export function getDCLInfo(): DCLInfo { 72 | return config 73 | } 74 | 75 | export function getConfig(network: string = networkFlag): DCLInfo { 76 | const envConfig = getEnvConfig() 77 | const dclInfoConfig = getDclInfoConfig() 78 | const defaultConfig = getDefaultConfig(network) 79 | const config = { ...defaultConfig, ...dclInfoConfig, ...envConfig } as DCLInfo 80 | return config 81 | } 82 | 83 | export function getCustomConfig(): Partial { 84 | const envConfig = getEnvConfig() 85 | const dclInfoConfig = getDclInfoConfig() 86 | return { ...dclInfoConfig, ...envConfig } 87 | } 88 | 89 | function getDefaultConfig(network: string): Partial { 90 | const isMainnet = network === 'mainnet' 91 | return { 92 | userId: '', 93 | trackStats: false, 94 | provider: isDevelopment() ? 'https://sepolia.infura.io/' : 'https://mainnet.infura.io/', 95 | MANAToken: isMainnet ? '0x0f5d2fb29fb7d3cfee444a200298f468908cc942' : '0xfa04d2e2ba9aec166c93dfeeba7427b2303befa9', 96 | LANDRegistry: isMainnet 97 | ? '0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d' 98 | : '0x42f4ba48791e2de32f5fbf553441c2672864bb33', 99 | EstateRegistry: isMainnet 100 | ? '0x959e104e1a4db6317fa58f8295f586e1a978c297' 101 | : '0x369a7fbe718c870c79f99fb423882e8dd8b20486', 102 | catalystUrl: isMainnet ? 'https://peer.decentraland.org' : 'https://peer-ue-2.decentraland.zone', 103 | dclApiUrl: isMainnet 104 | ? 'https://subgraph.decentraland.org/land-manager' 105 | : 'https://api.studio.thegraph.com/query/49472/land-manager-sepolia/version/latest', 106 | segmentKey: 107 | isStableVersion() && !isDevelopment() ? 'sFdziRVDJo0taOnGzTZwafEL9nLIANZ3' : 'mjCV5Dc4VAKXLJAH5g7LyHyW1jrIR3to' 108 | } 109 | } 110 | 111 | function getDclInfoConfig(): Partial { 112 | const dclInfo = getDCLInfo() 113 | const fileExists = !!dclInfo 114 | if (!fileExists) { 115 | return { fileExists } 116 | } 117 | 118 | const dclInfoConfig = { 119 | fileExists, 120 | userId: dclInfo.userId, 121 | trackStats: !!dclInfo.trackStats, 122 | MANAToken: dclInfo.MANAToken, 123 | LANDRegistry: dclInfo.LANDRegistry, 124 | EstateRegistry: dclInfo.EstateRegistry, 125 | catalystUrl: dclInfo.catalystUrl, 126 | segmentKey: dclInfo.segmentKey 127 | } 128 | 129 | return removeEmptyKeys(dclInfoConfig) 130 | } 131 | 132 | function getEnvConfig(): Partial { 133 | const { RPC_URL, MANA_TOKEN, LAND_REGISTRY, ESTATE_REGISTRY, CONTENT_URL, SEGMENT_KEY } = process.env 134 | 135 | const envConfig = { 136 | provider: RPC_URL, 137 | MANAToken: MANA_TOKEN, 138 | LANDRegistry: LAND_REGISTRY, 139 | EstateRegistry: ESTATE_REGISTRY, 140 | contentUrl: CONTENT_URL, 141 | segmentKey: SEGMENT_KEY 142 | } 143 | 144 | return removeEmptyKeys(envConfig) 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/moduleHelpers.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { spawn } from 'child_process' 3 | import semver from 'semver' 4 | import fetch from 'node-fetch' 5 | import packageJson from 'package-json' 6 | 7 | import * as spinner from '../utils/spinner' 8 | import { readJSON } from '../utils/filesystem' 9 | import { getNodeModulesPath } from '../utils/project' 10 | 11 | export const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm' 12 | let version: string | null = null 13 | 14 | export function setVersion(v: string) { 15 | version = v 16 | } 17 | 18 | export function buildTypescript({ 19 | workingDir, 20 | watch, 21 | production, 22 | silence = false 23 | }: { 24 | workingDir: string 25 | watch: boolean 26 | production: boolean 27 | silence?: boolean 28 | }): Promise { 29 | const command = watch ? 'watch' : 'build' 30 | const NODE_ENV = production ? 'production' : '' 31 | 32 | return new Promise((resolve, reject) => { 33 | const child = spawn(npm, ['run', command], { 34 | shell: true, 35 | cwd: workingDir, 36 | env: { ...process.env, NODE_ENV } 37 | }) 38 | 39 | if (!silence) { 40 | child.stdout.pipe(process.stdout) 41 | child.stderr.pipe(process.stderr) 42 | } 43 | 44 | child.stdout.on('data', (data) => { 45 | if (data.toString().indexOf('The compiler is watching file changes...') !== -1) { 46 | if (!silence) spinner.succeed('Project built.') 47 | return resolve() 48 | } 49 | }) 50 | 51 | child.on('close', (code) => { 52 | if (code !== 0) { 53 | const msg = 'Error while building the project' 54 | if (!silence) spinner.fail(msg) 55 | reject(new Error(msg)) 56 | } else { 57 | if (!silence) spinner.succeed('Project built.') 58 | return resolve() 59 | } 60 | }) 61 | }) 62 | } 63 | 64 | export async function getLatestVersion(name: string): Promise { 65 | if (!(await isOnline())) { 66 | return '' 67 | } 68 | 69 | try { 70 | // NOTE: this packageJson function should receive the workingDir 71 | const pkg = await packageJson(name.toLowerCase()) 72 | return pkg.version as string 73 | } catch (e) { 74 | return '' 75 | } 76 | } 77 | 78 | export async function getInstalledVersion(workingDir: string, name: string): Promise { 79 | let decentralandApiPkg: { version: string } 80 | 81 | try { 82 | decentralandApiPkg = await readJSON<{ version: string }>( 83 | path.resolve(getNodeModulesPath(workingDir), name, 'package.json') 84 | ) 85 | } catch (e) { 86 | return '' 87 | } 88 | 89 | return decentralandApiPkg.version 90 | } 91 | 92 | export async function getOutdatedEcs(workingDir: string): Promise< 93 | | { 94 | package: string 95 | installedVersion: string 96 | latestVersion: string 97 | } 98 | | undefined 99 | > { 100 | const decentralandEcs6Version = await getInstalledVersion(workingDir, 'decentraland-ecs') 101 | 102 | if (decentralandEcs6Version) { 103 | const latestVersion = await getLatestVersion('decentraland-ecs') 104 | if (latestVersion && semver.lt(decentralandEcs6Version, latestVersion)) { 105 | return { 106 | package: 'decentraland-ecs', 107 | installedVersion: decentralandEcs6Version, 108 | latestVersion 109 | } 110 | } 111 | } 112 | return undefined 113 | } 114 | 115 | export async function getCLIPackageJson(): Promise { 116 | return readJSON(path.resolve(__dirname, '..', '..', 'package.json')) 117 | } 118 | 119 | export function getInstalledCLIVersion(): string { 120 | // eslint-disable-next-line @typescript-eslint/no-var-requires 121 | return version || require('../../package.json').version 122 | } 123 | 124 | export function isStableVersion(): boolean { 125 | return !getInstalledCLIVersion().includes('commit') 126 | } 127 | 128 | export async function isCLIOutdated(): Promise { 129 | const cliVersion = getInstalledCLIVersion() 130 | const cliVersionLatest = await getLatestVersion('decentraland') 131 | 132 | if (cliVersionLatest && cliVersion && semver.lt(cliVersion, cliVersionLatest)) { 133 | return true 134 | } else { 135 | return false 136 | } 137 | } 138 | 139 | export function isOnline(): Promise { 140 | return new Promise((resolve) => { 141 | fetch('https://decentraland.org/ping') 142 | .then(() => resolve(true)) 143 | .catch(() => resolve(false)) 144 | setTimeout(() => { 145 | resolve(false) 146 | }, 4000) 147 | }) 148 | } 149 | 150 | export async function isECSVersionLower(workingDir: string, version: string): Promise { 151 | const ecsPackageJson = await readJSON<{ 152 | version: string 153 | }>(path.resolve(getNodeModulesPath(workingDir), 'decentraland-ecs', 'package.json')) 154 | 155 | if (semver.lt(ecsPackageJson.version, version)) { 156 | return true 157 | } 158 | return false 159 | } 160 | -------------------------------------------------------------------------------- /test/e2e/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import test from 'ava' 4 | import puppeteer from 'puppeteer' 5 | 6 | import { isDebug } from '../../src/utils/env' 7 | import Commando, { Response } from '../helpers/commando' 8 | import sandbox from '../helpers/sandbox' 9 | 10 | function initProject(dirPath: string): Promise { 11 | return new Promise((resolve, reject) => { 12 | new Commando(`node ${path.resolve('dist', 'index.js')} init`, { 13 | silent: !isDebug(), 14 | workingDir: dirPath, 15 | env: { NODE_ENV: 'development' } 16 | }) 17 | .when(/Send anonymous usage stats to Decentraland?/, () => Response.NO) 18 | .endWhen(/Installing dependencies/) 19 | .on('err', reject) 20 | .on('end', resolve) 21 | }) 22 | } 23 | 24 | function startProject(dirPath): Promise { 25 | return new Promise((resolve) => { 26 | const command = new Commando(`node ${path.resolve('dist', 'index.js')} start --no-browser`, { 27 | silent: !isDebug(), 28 | workingDir: dirPath, 29 | env: { NODE_ENV: 'development' } 30 | }).when(/to exit/, async () => { 31 | resolve(command) 32 | }) 33 | }) 34 | } 35 | 36 | function statusProject(): Promise { 37 | return new Promise((resolve) => { 38 | let allData = '' 39 | new Commando( 40 | `node ${path.resolve('dist', 'index.js')} status --network sepolia 0,0`, 41 | { 42 | silent: !isDebug(), 43 | workingDir: '.', 44 | env: { NODE_ENV: 'development' } 45 | }, 46 | (data) => (allData += data) 47 | ).on('end', async () => { 48 | resolve(allData) 49 | }) 50 | }) 51 | } 52 | 53 | function deployProject(dirPath): Promise { 54 | return new Promise((resolve) => { 55 | new Commando(`node ${path.resolve('dist', 'index.js')} deploy --yes --network sepolia`, { 56 | silent: !isDebug(), 57 | workingDir: dirPath, 58 | env: { 59 | NODE_ENV: 'development', 60 | DCL_PRIVATE_KEY: process.env.CI_DCL_PRIVATE_KEY 61 | } 62 | }).on('end', async () => { 63 | resolve() 64 | }) 65 | }) 66 | } 67 | 68 | test('E2E - full new user workflow of CLI (only CI test)', async (t) => { 69 | if (!process.env.CI_DCL_PRIVATE_KEY) { 70 | return t.pass('Missing CI_DCL_PRIVATE_KEY for full CI test') 71 | } 72 | 73 | let browser: puppeteer.Browser 74 | 75 | try { 76 | await sandbox(async (dirPath, done) => { 77 | await initProject(dirPath) 78 | 79 | // Remove rotation line 80 | let gameFile = await fs.readFile(path.resolve(dirPath, 'src', 'game.ts'), { 81 | encoding: 'utf8' 82 | }) 83 | gameFile = gameFile.replace('engine.addSystem(new RotatorSystem())', '') 84 | await fs.writeFile(path.resolve(dirPath, 'src', 'game.ts'), gameFile, { 85 | encoding: 'utf8' 86 | }) 87 | 88 | const startCmd = await startProject(dirPath) 89 | 90 | browser = await puppeteer.launch({ headless: !isDebug() }) 91 | 92 | // Assert if preview shows the cube 93 | const page = await browser.newPage() 94 | await page.goto('http://localhost:8000') 95 | await page.waitForSelector('#main-canvas') 96 | 97 | const snapshotPreview = await fs.readFile(path.resolve(__dirname, './snapshots/dcl-preview.png')) 98 | const imagePreview = await page.screenshot({ 99 | encoding: 'binary', 100 | path: path.resolve(dirPath, 'dcl-preview.png') 101 | }) 102 | 103 | t.is(Buffer.compare(snapshotPreview, imagePreview), 0) 104 | 105 | // With this random content we can change the CID and verify successful deployment 106 | const randomString = Math.random().toString(36).substring(7) 107 | 108 | // Assert that hotreloading changes preview 109 | gameFile = gameFile.replace('spawnCube(8, 1, 8)', `spawnCube(5, 5, 5) // ${randomString}`) 110 | await fs.writeFile(path.resolve(dirPath, 'src', 'game.ts'), gameFile, { 111 | encoding: 'utf8' 112 | }) 113 | await page.reload() 114 | await page.waitForSelector('#main-canvas') 115 | const [snapshotModified1, snapshotModified2] = await Promise.all([ 116 | fs.readFile(path.resolve(__dirname, './snapshots/dcl-preview-modified.1.png')), 117 | fs.readFile(path.resolve(__dirname, './snapshots/dcl-preview-modified.2.png')) 118 | ]) 119 | 120 | const imageModified = await page.screenshot({ 121 | encoding: 'binary', 122 | path: path.resolve(dirPath, 'dcl-preview-modified.png') 123 | }) 124 | 125 | startCmd.end() 126 | void browser.close() 127 | t.true( 128 | Buffer.compare(snapshotModified1, imageModified) === 0 || Buffer.compare(snapshotModified2, imageModified) === 0 129 | ) 130 | 131 | const statusBefore = await statusProject() 132 | await deployProject(dirPath) 133 | const statusAfter = await statusProject() 134 | t.not(statusBefore, statusAfter) 135 | 136 | done() 137 | }) 138 | } catch (error) { 139 | if (!isDebug()) { 140 | void browser.close() 141 | } 142 | throw error 143 | } 144 | }) 145 | -------------------------------------------------------------------------------- /src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import AnalyticsNode from 'analytics-node' 3 | import { createDCLInfo, getConfig } from '../config' 4 | import { isOnline, getInstalledCLIVersion } from './moduleHelpers' 5 | import { debug } from './logging' 6 | import chalk from 'chalk' 7 | import { IPFSv2 } from '@dcl/schemas' 8 | 9 | // Setup segment.io 10 | const SINGLEUSER = 'cli-user' 11 | 12 | export let analytics: AnalyticsNode 13 | 14 | const ANONYMOUS_DATA_QUESTION = 'Send Anonymous data' 15 | 16 | function isCI() { 17 | return process.env.CI === 'true' || process.argv.includes('--ci') || process.argv.includes('--c') 18 | } 19 | 20 | function isEditor() { 21 | return process.env.EDITOR === 'true' 22 | } 23 | 24 | export type AnalyticsProject = { 25 | projectHash?: string 26 | ecs?: { 27 | ecsVersion: string 28 | packageVersion: string 29 | } 30 | coords?: { x: number; y: number } 31 | parcelCount?: number 32 | isWorkspace: boolean 33 | } 34 | 35 | export type SceneDeploySuccess = Omit & { 36 | isWorld: boolean 37 | sceneId: IPFSv2 38 | targetContentServer: string 39 | worldName: string | undefined 40 | } 41 | 42 | export namespace Analytics { 43 | export const sceneCreated = (properties?: { projectType: string; url?: string }) => 44 | trackAsync('Scene created', properties) 45 | 46 | export const startPreview = (properties: AnalyticsProject) => trackAsync('Preview started', properties) 47 | 48 | export const sceneStartDeploy = (properties?: any) => trackAsync('Scene deploy started', properties) 49 | 50 | export const sceneDeploySuccess = (properties: SceneDeploySuccess) => trackAsync('Scene deploy success', properties) 51 | 52 | export const worldAcl = (properties: any) => trackAsync('World ACL', properties) 53 | 54 | export const buildScene = (properties: AnalyticsProject) => trackAsync('Build scene', properties) 55 | 56 | export const infoCmd = (properties?: any) => trackAsync('Info command', properties) 57 | export const statusCmd = (properties?: any) => trackAsync('Status command', properties) 58 | export const sendData = (shareData: boolean) => trackAsync(ANONYMOUS_DATA_QUESTION, { shareData }) 59 | export const tryToUseDeprecated = (properties?: any) => trackAsync('Try to use depacreated feature', properties) 60 | 61 | export async function identify(devId: string) { 62 | analytics.identify({ 63 | userId: SINGLEUSER, 64 | traits: { 65 | os: process.platform, 66 | createdAt: new Date().getTime(), 67 | isCI: isCI(), 68 | isEditor: isEditor(), 69 | devId 70 | } 71 | }) 72 | } 73 | 74 | export async function reportError(type: string, message: string, stackTrace: string) { 75 | return track('Error', { 76 | errorType: type, 77 | message, 78 | stackTrace 79 | }) 80 | } 81 | 82 | export async function requestPermission() { 83 | const { fileExists, segmentKey } = getConfig() 84 | if (!segmentKey) return 85 | analytics = new AnalyticsNode(segmentKey) 86 | if (!fileExists) { 87 | console.log( 88 | chalk.dim( 89 | `Decentraland CLI sends anonymous usage stats to improve their products, if you want to disable it change the configuration at ${chalk.bold( 90 | '~/.dclinfo' 91 | )}\n` 92 | ) 93 | ) 94 | 95 | const newUserId = uuidv4() 96 | await createDCLInfo({ userId: newUserId, trackStats: true }) 97 | debug(`${chalk.bold('.dclinfo')} file created`) 98 | await identify(newUserId) 99 | sendData(true) 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Tracks an specific event using the Segment API 106 | * @param eventName The name of the event to be tracked 107 | * @param properties Any object containing serializable data 108 | */ 109 | async function track(eventName: string, properties: any = {}) { 110 | const { userId, trackStats } = getConfig() 111 | 112 | if (!(await isOnline())) { 113 | return null 114 | } 115 | 116 | return new Promise(async (resolve) => { 117 | const newProperties = { 118 | ...properties, 119 | os: process.platform, 120 | nodeVersion: process.version, 121 | cliVersion: getInstalledCLIVersion(), 122 | isCI: isCI(), 123 | isEditor: isEditor(), 124 | devId: userId 125 | } 126 | 127 | const shouldTrack = trackStats || eventName === ANONYMOUS_DATA_QUESTION 128 | if (!shouldTrack) { 129 | resolve() 130 | } 131 | 132 | const event = { 133 | userId: SINGLEUSER, 134 | event: eventName, 135 | properties: newProperties 136 | } 137 | 138 | try { 139 | analytics.track(event, () => { 140 | resolve() 141 | }) 142 | } catch (e) { 143 | resolve() 144 | } 145 | }) 146 | } 147 | 148 | const pendingTracking: Promise[] = [] 149 | 150 | function trackAsync(eventName: string, properties: any = {}) { 151 | const pTracking = track(eventName, properties).then().catch(debug) 152 | pendingTracking.push(pTracking) 153 | } 154 | 155 | export async function finishPendingTracking() { 156 | return Promise.all(pendingTracking) 157 | } 158 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | import chalk from 'chalk' 3 | 4 | import * as log from './utils/logging' 5 | import { finishPendingTracking, Analytics } from './utils/analytics' 6 | import { getInstalledCLIVersion, setVersion } from './utils/moduleHelpers' 7 | import { loadConfig } from './config' 8 | import commands from './commands' 9 | import { checkNodeAndNpmVersion } from './utils/nodeAndNpmVersion' 10 | 11 | log.debug(`Running with NODE_ENV: ${process.env.NODE_ENV}`) 12 | log.debug(`Provided argv: ${JSON.stringify(process.argv)}`) 13 | 14 | process.on('unhandledRejection', (error: any) => { 15 | if (error?.config?.url?.includes('.segment.')) { 16 | console.log(chalk.dim('\n⚠️ Analytics api call failed. \n')) 17 | return 18 | } 19 | 20 | throw error 21 | }) 22 | 23 | const args = arg( 24 | { 25 | '--help': Boolean, 26 | '--version': Boolean, 27 | '--network': String, 28 | '-h': '--help', 29 | '-v': '--version', 30 | '-n': '--network' 31 | }, 32 | { 33 | permissive: true 34 | } 35 | ) 36 | log.debug(`Parsed args: ${JSON.stringify(args)}`) 37 | 38 | const subcommand = args._[0] 39 | log.debug(`Selected command ${chalk.bold(subcommand)}`) 40 | 41 | const help = ` 42 | ${chalk.bold('Decentraland CLI')} 43 | 44 | Usage: ${chalk.bold('dcl [command] [options]')} 45 | 46 | ${chalk.dim('Commands:')} 47 | 48 | init Create a new Decentraland Scene project 49 | build Build scene 50 | start Start a local development server for a Decentraland Scene 51 | install Sync decentraland libraries in bundleDependencies 52 | install package Install a package 53 | deploy Upload scene to a particular Decentraland's Content server 54 | deploy-deprecated Upload scene to Decentraland's legacy content server (deprecated). 55 | export Export scene to static website format (HTML, JS and CSS) 56 | info [args] Displays information about a LAND, an Estate or an address 57 | status [args] Displays the deployment status of the project or a given LAND 58 | help [cmd] Displays complete help for given command 59 | version Display current version of dcl 60 | coords Set the parcels in your scene 61 | world-acl [args] Manage DCL worlds permissions, dcl help world-acl for more information. 62 | workspace subcommand Make a workspace level action, dcl help workspace for more information. 63 | 64 | ${chalk.dim('Options:')} 65 | 66 | -h, --help Displays complete help for used command or subcommand 67 | -v, --version Display current version of dcl 68 | 69 | ${chalk.dim('Example:')} 70 | 71 | - Show complete help for the subcommand "${chalk.dim('deploy')}" 72 | 73 | ${chalk.green('$ dcl help deploy')} 74 | ` 75 | 76 | export async function main(version: string) { 77 | await checkNodeAndNpmVersion() 78 | 79 | setVersion(version) 80 | if (!process.argv.includes('--ci') && !process.argv.includes('--c')) { 81 | const network = args['--network'] 82 | if (network && network !== 'mainnet' && network !== 'sepolia') { 83 | console.error( 84 | log.error( 85 | `The only available values for ${chalk.bold(`'--network'`)} are ${chalk.bold(`'mainnet'`)} or ${chalk.bold( 86 | `'sepolia'` 87 | )}` 88 | ) 89 | ) 90 | process.exit(1) 91 | } 92 | 93 | await loadConfig(network || 'mainnet') 94 | await Analytics.requestPermission() 95 | } 96 | 97 | if (subcommand === 'version' || args['--version']) { 98 | console.log(getInstalledCLIVersion()) 99 | return 100 | } 101 | 102 | if (!subcommand) { 103 | console.log(help) 104 | return 105 | } 106 | 107 | if (subcommand === 'help' || args['--help']) { 108 | const command = subcommand === 'help' ? args._[1] : subcommand 109 | if (commands.has(command) && command !== 'help') { 110 | try { 111 | const { help } = await import(`./commands/${command}`) 112 | console.log(help()) 113 | } catch (e: any) { 114 | console.error(log.error(e.message)) 115 | } 116 | return 117 | } 118 | console.log(help) 119 | return 120 | } 121 | 122 | if (!commands.has(subcommand)) { 123 | if (subcommand.startsWith('-')) { 124 | console.error( 125 | log.error( 126 | `The "${chalk.bold(subcommand)}" option does not exist, run ${chalk.bold('"dcl help"')} for more info.` 127 | ) 128 | ) 129 | process.exit(1) 130 | } 131 | console.error( 132 | log.error( 133 | `The "${chalk.bold(subcommand)}" subcommand does not exist, run ${chalk.bold('"dcl help"')} for more info.` 134 | ) 135 | ) 136 | process.exit(1) 137 | } 138 | 139 | try { 140 | const command = await import(`./commands/${subcommand}`) 141 | await command.main() 142 | await finishPendingTracking() 143 | } catch (e: any) { 144 | console.error( 145 | log.error( 146 | `\`${chalk.green(`dcl ${subcommand}`)}\` ${e.message}, run ${chalk.bold( 147 | `"dcl help ${subcommand}"` 148 | )} for more info.` 149 | ) 150 | ) 151 | 152 | await Analytics.reportError('Command error', e.message, e.stack) 153 | log.debug(e) 154 | process.exit(1) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/lib/Preview.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import WebSocket from 'ws' 3 | import fs from 'fs-extra' 4 | 5 | import chokidar from 'chokidar' 6 | import { default as ignore } from 'ignore' 7 | import { sdk } from '@dcl/schemas' 8 | import { fail, ErrorType } from '../utils/errors' 9 | import { Decentraland } from './Decentraland' 10 | import { setupBffAndComms } from './controllers/bff' 11 | import { 12 | ILoggerComponent, 13 | IMetricsComponent, 14 | IHttpServerComponent, 15 | IConfigComponent 16 | } from '@well-known-components/interfaces' 17 | import { HTTPProvider } from 'eth-connect' 18 | import { RoomComponent } from '@dcl/mini-comms/dist/adapters/rooms' 19 | import { Router } from '@well-known-components/http-server' 20 | import { WebSocketComponent } from './adapters/ws' 21 | import { setupCommsV1 } from './controllers/legacy-comms-v1' 22 | import { setupDebuggingAdapter } from './controllers/debugger' 23 | import { setupEcs6Endpoints } from './controllers/ecs6-endpoints' 24 | import { upgradeWebSocketResponse } from '@well-known-components/http-server/dist/ws' 25 | 26 | export type PreviewComponents = { 27 | logs: ILoggerComponent 28 | server: IHttpServerComponent 29 | config: IConfigComponent 30 | metrics: IMetricsComponent 31 | ethereumProvider: HTTPProvider 32 | rooms: RoomComponent 33 | dcl: Decentraland 34 | ws: WebSocketComponent 35 | } 36 | 37 | export async function wirePreview(components: PreviewComponents, watch: boolean) { 38 | const npmModulesPath = path.resolve(components.dcl.getWorkingDir(), 'node_modules') 39 | 40 | // TODO: dcl.project.needsDependencies() should do this 41 | if (!fs.pathExistsSync(npmModulesPath)) { 42 | fail(ErrorType.PREVIEW_ERROR, `Couldn\'t find ${npmModulesPath}, please run: npm install`) 43 | } 44 | 45 | const proxySetupPathEcs6 = path.resolve( 46 | components.dcl.getWorkingDir(), 47 | 'node_modules', 48 | 'decentraland-ecs', 49 | 'src', 50 | 'setupProxyV2.js' 51 | ) 52 | 53 | const proxySetupPathEcs7 = path.resolve( 54 | components.dcl.getWorkingDir(), 55 | 'node_modules', 56 | '@dcl', 57 | 'sdk', 58 | 'src', 59 | 'setupProxyV2.js' 60 | ) 61 | 62 | // this should come BEFORE the custom proxy 63 | const proxySetupPath = fs.existsSync(proxySetupPathEcs7) ? proxySetupPathEcs7 : proxySetupPathEcs6 64 | 65 | const router = new Router() 66 | const sceneUpdateClients = new Set() 67 | 68 | // handle old comms 69 | router.get('/', async (ctx, next) => { 70 | if (ctx.request.headers.get('upgrade') === 'websocket') { 71 | const userId = ctx.url.searchParams.get('identity') as string 72 | if (userId) { 73 | return upgradeWebSocketResponse((ws: WebSocket) => { 74 | adoptWebSocket(ws, userId) 75 | }) 76 | } else { 77 | return upgradeWebSocketResponse((ws: WebSocket) => { 78 | if (ws.readyState === ws.OPEN) { 79 | sceneUpdateClients.add(ws) 80 | } else { 81 | ws.on('open', () => sceneUpdateClients.add(ws)) 82 | } 83 | ws.on('close', () => sceneUpdateClients.delete(ws)) 84 | }) 85 | } 86 | } 87 | 88 | return next() 89 | }) 90 | 91 | await setupBffAndComms(components, router) 92 | const { adoptWebSocket } = setupCommsV1(components, router) 93 | await setupDebuggingAdapter(components, router) 94 | await setupEcs6Endpoints(components, router) 95 | if (watch) { 96 | await bindWatch(components, router, sceneUpdateClients) 97 | } 98 | 99 | components.server.setContext(components) 100 | components.server.use(router.allowedMethods()) 101 | components.server.use(router.middleware()) 102 | 103 | if (fs.existsSync(proxySetupPath)) { 104 | try { 105 | // eslint-disable-next-line @typescript-eslint/no-var-requires 106 | const setupProxy = require(proxySetupPath) 107 | setupProxy(router, components) 108 | } catch (err) { 109 | console.log(`${proxySetupPath} found but it couldn't be loaded properly`, err) 110 | } 111 | } 112 | } 113 | 114 | function debounce void>(callback: T, delay: number) { 115 | let debounceTimer: NodeJS.Timeout 116 | return (...args: Parameters) => { 117 | clearTimeout(debounceTimer) 118 | debounceTimer = setTimeout(() => callback(...args), delay) 119 | } 120 | } 121 | 122 | async function bindWatch( 123 | components: PreviewComponents, 124 | router: Router, 125 | sceneUpdateClients: Set 126 | ) { 127 | for (const project of components.dcl.workspace.getAllProjects()) { 128 | const ig = ignore().add((await project.getDCLIgnore())!) 129 | const { sceneId, sceneType } = project.getInfo() 130 | const sceneFile = await project.getSceneFile() 131 | chokidar.watch(project.getProjectWorkingDir()).on( 132 | 'all', 133 | debounce((_, pathWatch) => { 134 | // if the updated file is the scene.json#main then skip all drop tests 135 | if (path.resolve(pathWatch) !== path.resolve(project.getProjectWorkingDir(), sceneFile.main)) { 136 | if (ig.ignores(pathWatch)) { 137 | return 138 | } 139 | 140 | // ignore source files 141 | if (pathWatch.endsWith('.ts') || pathWatch.endsWith('.tsx')) { 142 | return 143 | } 144 | } 145 | 146 | sceneUpdateClients.forEach((ws) => { 147 | if (ws.readyState === WebSocket.OPEN) { 148 | const message: sdk.SceneUpdate = { 149 | type: sdk.SCENE_UPDATE, 150 | payload: { sceneId, sceneType } 151 | } 152 | 153 | ws.send(sdk.UPDATE) 154 | ws.send(JSON.stringify(message)) 155 | } 156 | }) 157 | }, 500) 158 | ) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/commands/info.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | import chalk from 'chalk' 3 | 4 | import { Decentraland, Estate, ParcelMetadata } from '../lib/Decentraland' 5 | import { formatDictionary, debug } from '../utils/logging' 6 | import { Analytics } from '../utils/analytics' 7 | import { getObject, getString, isValid } from '../utils/coordinateHelpers' 8 | import { fail, ErrorType } from '../utils/errors' 9 | import { parseTarget } from '../utils/land' 10 | import * as spinner from '../utils/spinner' 11 | 12 | export const help = () => ` 13 | Usage: ${chalk.bold('dcl info [target] [options]')} 14 | 15 | ${chalk.dim('Options:')} 16 | 17 | -h, --help Displays complete help 18 | -b, --blockchain Retrieve information directly from the blockchain instead of Decentraland remote API 19 | -n, --network Choose between ${chalk.bold('mainnet')} and ${chalk.bold('sepolia')} (default 'mainnet') 20 | 21 | 22 | ${chalk.dim('Examples:')} 23 | 24 | - Get information from the ${chalk.bold('LAND')} located at "${chalk.bold('-12, 40')}" 25 | 26 | ${chalk.green('$ dcl info -12,40')} 27 | 28 | - Get information from the ${chalk.bold('estate')} with ID "${chalk.bold('5')}" directly from blockchain provider 29 | 30 | ${chalk.green('$ dcl info 5 --blockchain')} 31 | 32 | - Get information from the ${chalk.bold('address 0x8bed95d830475691c10281f1fea2c0a0fe51304b')}" 33 | 34 | ${chalk.green('$ dcl info 0x8bed95d830475691c10281f1fea2c0a0fe51304b')} 35 | ` 36 | 37 | function getTargetType(value: string): string { 38 | if (isValid(value)) { 39 | return 'parcel' 40 | } 41 | 42 | const id = parseInt(value, 10) 43 | if (Number.isInteger(id) && id > 0) { 44 | return 'estate' 45 | } 46 | 47 | if (value.startsWith('0x')) { 48 | return 'address' 49 | } 50 | 51 | return '' 52 | } 53 | 54 | export async function main() { 55 | const args = arg( 56 | { 57 | '--help': Boolean, 58 | '--blockchain': Boolean, 59 | '--network': String, 60 | '-h': '--help', 61 | '-b': '--blockchain', 62 | '-n': '--network' 63 | }, 64 | { permissive: true } 65 | ) 66 | 67 | if (!args._[1]) { 68 | fail(ErrorType.INFO_ERROR, 'Please provide a target to retrieve data') 69 | } 70 | 71 | const target = parseTarget(args._) 72 | debug(`Parsed target: ${target}`) 73 | const type = getTargetType(target) 74 | 75 | if (!type) { 76 | fail(ErrorType.INFO_ERROR, `Invalid target "${chalk.bold(target)}"`) 77 | } 78 | 79 | const dcl = new Decentraland({ 80 | blockchain: args['--blockchain'], 81 | workingDir: process.cwd() 82 | }) 83 | 84 | if (type === 'parcel') { 85 | spinner.create(chalk.dim(`Fetching information for LAND ${target}`)) 86 | const coords = getObject(target) 87 | Analytics.infoCmd({ type: 'coordinates', target: coords }) 88 | const [estate, data] = await Promise.all([dcl.getEstateOfParcel(coords), dcl.getParcelInfo(coords)]) 89 | const output = estate ? { ...data, estate } : data 90 | spinner.succeed(`Fetched data for LAND ${chalk.bold(target)}`) 91 | logParcel(output) 92 | return 93 | } 94 | 95 | if (type === 'estate') { 96 | spinner.create(chalk.dim(`Fetching information for Estate ${target}`)) 97 | const estateId = parseInt(target, 10) 98 | Analytics.infoCmd({ type: 'estate', target: estateId }) 99 | const estate = await dcl.getEstateInfo(estateId) 100 | spinner.succeed(`Fetched data for Estate ${chalk.bold(target)}`) 101 | logEstate(estate, estateId) 102 | return 103 | } 104 | 105 | spinner.create(chalk.dim(`Fetching information for address ${target}`)) 106 | Analytics.infoCmd({ type: 'address', target: target }) 107 | const { parcels, estates } = await dcl.getAddressInfo(target) 108 | 109 | const formattedParcels = parcels.reduce((acc, parcel) => { 110 | return { 111 | ...acc, 112 | [`${parcel.x},${parcel.y}`]: { 113 | name: parcel.name, 114 | description: parcel.description 115 | } 116 | } 117 | }, {}) 118 | const formattedEstates = estates.reduce((acc, estate) => { 119 | return { 120 | ...acc, 121 | [`ID ${estate.id.toString()}`]: { 122 | name: estate.name, 123 | description: estate.description 124 | } 125 | } 126 | }, {}) 127 | 128 | spinner.succeed(`Fetched data for address ${chalk.bold(target)}`) 129 | 130 | if (parcels.length === 0 && estates.length === 0) { 131 | return console.log(chalk.italic('\n No information available\n')) 132 | } 133 | 134 | if (parcels.length !== 0) { 135 | console.log(`\n LAND owned by ${target}:\n`) 136 | console.log(formatDictionary(formattedParcels, { spacing: 2, padding: 2 })) 137 | } 138 | 139 | if (estates.length !== 0) { 140 | console.log(`\n Estates owned by ${target}:\n`) 141 | console.log(formatDictionary(formattedEstates, { spacing: 2, padding: 2 })) 142 | } 143 | } 144 | 145 | function logParcel(output: Partial & { estate?: Estate }) { 146 | console.log('\n Scene Metadata:\n') 147 | 148 | if (output.scene) { 149 | console.log(formatDictionary(output.scene, { spacing: 2, padding: 2 })) 150 | } else { 151 | console.log(chalk.italic(' No information available\n')) 152 | } 153 | 154 | console.log(' LAND Metadata:\n') 155 | 156 | if (output.land) { 157 | console.log(formatDictionary(output.land, { spacing: 2, padding: 2 })) 158 | } else { 159 | console.log(chalk.italic(' No information available\n')) 160 | } 161 | 162 | if (output.estate) { 163 | logEstate(output.estate) 164 | } 165 | } 166 | 167 | function logEstate(estate?: Estate, id?: number) { 168 | if (!estate) { 169 | console.log(chalk.italic(`\n Estate with ID ${id} doesn't exist\n`)) 170 | return 171 | } 172 | 173 | if (estate.parcels.length === 0) { 174 | console.log(chalk.bold(`\n Estate with ID ${id} has been dissolved\n`)) 175 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 176 | // @ts-ignore: TODO 177 | delete estate.parcels 178 | } 179 | 180 | console.log(' Estate Metadata:\n') 181 | 182 | if (estate) { 183 | const estateInfo = { ...estate, parcels: singleLineParcels(estate.parcels) } 184 | console.log(formatDictionary(estateInfo, { spacing: 2, padding: 2 })) 185 | } 186 | } 187 | 188 | function singleLineParcels(parcels: any) { 189 | return parcels.map(getString).join('; ') 190 | } 191 | -------------------------------------------------------------------------------- /src/lib/adapters/proto/comms.d.ts: -------------------------------------------------------------------------------- 1 | // package: protocol 2 | // file: comms.proto 3 | 4 | import jspb from 'google-protobuf' 5 | 6 | export class AuthData extends jspb.Message { 7 | getSignature(): string 8 | setSignature(value: string): void 9 | 10 | getIdentity(): string 11 | setIdentity(value: string): void 12 | 13 | getTimestamp(): string 14 | setTimestamp(value: string): void 15 | 16 | getAccessToken(): string 17 | setAccessToken(value: string): void 18 | 19 | serializeBinary(): Uint8Array 20 | toObject(includeInstance?: boolean): AuthData.AsObject 21 | static toObject(includeInstance: boolean, msg: AuthData): AuthData.AsObject 22 | static extensions: { [key: number]: jspb.ExtensionFieldInfo } 23 | static extensionsBinary: { 24 | [key: number]: jspb.ExtensionFieldBinaryInfo 25 | } 26 | static serializeBinaryToWriter(message: AuthData, writer: jspb.BinaryWriter): void 27 | static deserializeBinary(bytes: Uint8Array): AuthData 28 | static deserializeBinaryFromReader(message: AuthData, reader: jspb.BinaryReader): AuthData 29 | } 30 | 31 | export namespace AuthData { 32 | export type AsObject = { 33 | signature: string 34 | identity: string 35 | timestamp: string 36 | accessToken: string 37 | } 38 | } 39 | 40 | export class DataHeader extends jspb.Message { 41 | getCategory(): CategoryMap[keyof CategoryMap] 42 | setCategory(value: CategoryMap[keyof CategoryMap]): void 43 | 44 | serializeBinary(): Uint8Array 45 | toObject(includeInstance?: boolean): DataHeader.AsObject 46 | static toObject(includeInstance: boolean, msg: DataHeader): DataHeader.AsObject 47 | static extensions: { [key: number]: jspb.ExtensionFieldInfo } 48 | static extensionsBinary: { 49 | [key: number]: jspb.ExtensionFieldBinaryInfo 50 | } 51 | static serializeBinaryToWriter(message: DataHeader, writer: jspb.BinaryWriter): void 52 | static deserializeBinary(bytes: Uint8Array): DataHeader 53 | static deserializeBinaryFromReader(message: DataHeader, reader: jspb.BinaryReader): DataHeader 54 | } 55 | 56 | export namespace DataHeader { 57 | export type AsObject = { 58 | category: CategoryMap[keyof CategoryMap] 59 | } 60 | } 61 | 62 | export class PositionData extends jspb.Message { 63 | getCategory(): CategoryMap[keyof CategoryMap] 64 | setCategory(value: CategoryMap[keyof CategoryMap]): void 65 | 66 | getTime(): number 67 | setTime(value: number): void 68 | 69 | getPositionX(): number 70 | setPositionX(value: number): void 71 | 72 | getPositionY(): number 73 | setPositionY(value: number): void 74 | 75 | getPositionZ(): number 76 | setPositionZ(value: number): void 77 | 78 | getRotationX(): number 79 | setRotationX(value: number): void 80 | 81 | getRotationY(): number 82 | setRotationY(value: number): void 83 | 84 | getRotationZ(): number 85 | setRotationZ(value: number): void 86 | 87 | getRotationW(): number 88 | setRotationW(value: number): void 89 | 90 | serializeBinary(): Uint8Array 91 | toObject(includeInstance?: boolean): PositionData.AsObject 92 | static toObject(includeInstance: boolean, msg: PositionData): PositionData.AsObject 93 | static extensions: { [key: number]: jspb.ExtensionFieldInfo } 94 | static extensionsBinary: { 95 | [key: number]: jspb.ExtensionFieldBinaryInfo 96 | } 97 | static serializeBinaryToWriter(message: PositionData, writer: jspb.BinaryWriter): void 98 | static deserializeBinary(bytes: Uint8Array): PositionData 99 | static deserializeBinaryFromReader(message: PositionData, reader: jspb.BinaryReader): PositionData 100 | } 101 | 102 | export namespace PositionData { 103 | export type AsObject = { 104 | category: CategoryMap[keyof CategoryMap] 105 | time: number 106 | positionX: number 107 | positionY: number 108 | positionZ: number 109 | rotationX: number 110 | rotationY: number 111 | rotationZ: number 112 | rotationW: number 113 | } 114 | } 115 | 116 | export class ProfileData extends jspb.Message { 117 | getCategory(): CategoryMap[keyof CategoryMap] 118 | setCategory(value: CategoryMap[keyof CategoryMap]): void 119 | 120 | getTime(): number 121 | setTime(value: number): void 122 | 123 | getProfileVersion(): string 124 | setProfileVersion(value: string): void 125 | 126 | serializeBinary(): Uint8Array 127 | toObject(includeInstance?: boolean): ProfileData.AsObject 128 | static toObject(includeInstance: boolean, msg: ProfileData): ProfileData.AsObject 129 | static extensions: { [key: number]: jspb.ExtensionFieldInfo } 130 | static extensionsBinary: { 131 | [key: number]: jspb.ExtensionFieldBinaryInfo 132 | } 133 | static serializeBinaryToWriter(message: ProfileData, writer: jspb.BinaryWriter): void 134 | static deserializeBinary(bytes: Uint8Array): ProfileData 135 | static deserializeBinaryFromReader(message: ProfileData, reader: jspb.BinaryReader): ProfileData 136 | } 137 | 138 | export namespace ProfileData { 139 | export type AsObject = { 140 | category: CategoryMap[keyof CategoryMap] 141 | time: number 142 | profileVersion: string 143 | } 144 | } 145 | 146 | export class ChatData extends jspb.Message { 147 | getCategory(): CategoryMap[keyof CategoryMap] 148 | setCategory(value: CategoryMap[keyof CategoryMap]): void 149 | 150 | getTime(): number 151 | setTime(value: number): void 152 | 153 | getMessageId(): string 154 | setMessageId(value: string): void 155 | 156 | getText(): string 157 | setText(value: string): void 158 | 159 | serializeBinary(): Uint8Array 160 | toObject(includeInstance?: boolean): ChatData.AsObject 161 | static toObject(includeInstance: boolean, msg: ChatData): ChatData.AsObject 162 | static extensions: { [key: number]: jspb.ExtensionFieldInfo } 163 | static extensionsBinary: { 164 | [key: number]: jspb.ExtensionFieldBinaryInfo 165 | } 166 | static serializeBinaryToWriter(message: ChatData, writer: jspb.BinaryWriter): void 167 | static deserializeBinary(bytes: Uint8Array): ChatData 168 | static deserializeBinaryFromReader(message: ChatData, reader: jspb.BinaryReader): ChatData 169 | } 170 | 171 | export namespace ChatData { 172 | export type AsObject = { 173 | category: CategoryMap[keyof CategoryMap] 174 | time: number 175 | messageId: string 176 | text: string 177 | } 178 | } 179 | 180 | export interface CategoryMap { 181 | UNKNOWN: 0 182 | POSITION: 1 183 | PROFILE: 2 184 | CHAT: 3 185 | SCENE_MESSAGE: 4 186 | } 187 | 188 | export const Category: CategoryMap 189 | --------------------------------------------------------------------------------