Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Question Best way to setup environment for local development of JS scripts for CC

Discussion in 'Cloud Code' started by ViliamVolosV, Feb 6, 2023.

  1. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    Hi CC Team.
    Im C#\Unity developer, not JS developer
    Im developing game that fully use yours Cloud solution. Economycs,Cloud Save, Cloud Code, Remote Config.
    After a few weeks of work, I realized that I needed to set up a local environment for JS development.
    For now i created folder in my project Scripts\CloudCode. And i code JS scripts in Rider :)
    Im using Deployment packcage for fast deployment.
    But im realy need IDE for autocomplete for lodash and unity services (economy, cloud-save) ,static analysis and etc.
    Allsow debugging would be greate
    For JS developing i want to use visual studio code

    Any advices would be greate
     
  2. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    Hi ViliamVolosV,

    We have indeed some references you could start with to get a better local setup to help you with JS development.

    First, make sure that you initialize the JS project in the Editor, this will enable a lot of the IDE features, including autocompletion on api to use for the other UGS services:
    • You will find this in "Unity/Edit > Preferences... > Cloud Code
      • Among the options there, there is an "Initialize JS Project" option, simply click this and apply.
    For the other use-cases you are looking for, we have different library that we show how to get setup with which can enable better IDE integration (ESLint), Test framework and running locally your scripts.

    https://docs.unity.com/cloud-code/e..._Code_JavaScript_projects_in_the_Unity_Editor (see all sections under this section)

    Please let us know if this help or if you have any questions.
     
  3. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    This is very helpfull.
    A couple of notes about documentation
    1 - In this article nothing said about "Initialize JS Project"
    2 - Part about using VSCode as editor for JS. Args: $(ProjectPath) $(File)" - missing " at the begining
     
  4. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    Some other qustions:
    As far as i understood for runnig localy my scripts in need to create test case for it.
    If it is true - can you provide simple example.
    This is my JS script for generating hero in tavern
    How i test unit CC libraries
    like this requests

    const getTimeResponse = await cloudSaveApi.getItems(projectId, playerId, [tavernDataKey]);
    const createdItem = await inventoryApi.addInventoryItem(requestParameters);

    TavernData
    Code (JavaScript):
    1. {
    2.     "heroesInTavern": ["bd17d622-54e4-4378-85e3-4ff32be1b0c7"],
    3.     "level": 1,
    4.     "slotsCount": 2
    5. }
    Code (JavaScript):
    1.  
    2.  
    3. const { InventoryApi } = require("@unity-services/economy-2.4");
    4. const { DataApi } = require("@unity-services/cloud-save-1.2");
    5. const _ = require("lodash-4.17");
    6. const badRequestError = 400;
    7. const tooManyRequestsError = 429;
    8. const tavernDataKey = "TavernData";
    9.  
    10. module.exports = async ({ params, context, logger }) => {
    11.     try {
    12.         const { projectId, playerId, accessToken } = context;
    13.         const cloudSaveApi = new DataApi({ accessToken });
    14.         const inventoryApi = new InventoryApi({ accessToken });
    15.  
    16.         const getTimeResponse = await cloudSaveApi.getItems(projectId, playerId, [tavernDataKey]);
    17.         const heroName = params.name;
    18.         const slotId = params.slotId;
    19.         let InvenoryItem = {};
    20.  
    21.         // Check for the last grant epoch time
    22.         if (getTimeResponse.data.results &&
    23.             getTimeResponse.data.results.length > 0 &&
    24.             getTimeResponse.data.results[0] &&
    25.             getTimeResponse.data.results[0].value) {
    26.  
    27.             const tavern = getTimeResponse.data.results[0].value;
    28.             const count = tavern.heroesInTavern.length - tavern.heroesInTavern.filter(String).length;
    29.  
    30.             if (tavern.heroesInTavern.length == tavern.slotsCount && count == 0) {
    31.                 throw Error(`No free slots in tavern`);
    32.             }
    33.  
    34.             logger.info(tavern);
    35.             const heroData = {
    36.                 "Name": heroName,
    37.                 "attributes":
    38.                     [
    39.                         { "Name": "Strength", "Value": 1 },
    40.                         { "Name": "Perception", "Value": 1 },
    41.                         { "Name": "Endurance", "Value": 1 },
    42.                         { "Name": "Charisma", "Value": 1 },
    43.                         { "Name": "Intelligence", "Value": 1 },
    44.                         { "Name": "Agility", "Value": 1 },
    45.                         { "Name": "Luck", "Value": 1 },
    46.                     ]
    47.             };
    48.  
    49.             const addInventoryRequest = {
    50.                 inventoryItemId: "HERO",
    51.                 instanceData: heroData
    52.             };
    53.             const requestParameters = { projectId, playerId, addInventoryRequest };
    54.             logger.info(requestParameters);
    55.             const createdItem = await inventoryApi.addInventoryItem(requestParameters);
    56.             logger.info(createdItem);
    57.  
    58.             tavern.heroesInTavern[slotId] = createdItem.data.playersInventoryItemId;
    59.             const tavernParams = { key: tavernDataKey, value: tavern };
    60.             const setTavernResponse = await cloudSaveApi.setItem(projectId, playerId, tavernParams);
    61.  
    62.             InvenoryItem = {
    63.                 InventoryId: createdItem.data.playersInventoryItemId,
    64.                 Item: heroData
    65.             }
    66.         }
    67.         return InvenoryItem;
    68.     } catch (error) {
    69.         transformAndThrowCaughtError(error);
    70.     }
    71. };
    72.  
    73.  
    74. // Some form of this function appears in all Cloud Code scripts.
    75. // Its purpose is to parse the errors thrown from the script into a standard exception object which can be stringified.
    76. function transformAndThrowCaughtError(error) {
    77.     let result = {
    78.         status: 0,
    79.         name: "",
    80.         message: "",
    81.         retryAfter: null,
    82.         details: ""
    83.     };
    84.  
    85.     if (error.response) {
    86.         result.status = error.response.data.status ? error.response.data.status : 0;
    87.         result.name = error.response.data.title ? error.response.data.title : "Unknown Error";
    88.         result.message = error.response.data.detail ? error.response.data.detail : error.response.data;
    89.  
    90.         if (error.response.status === tooManyRequestsError) {
    91.             result.retryAfter = error.response.headers['retry-after'];
    92.         } else if (error.response.status === badRequestError) {
    93.             let arr = [];
    94.  
    95.             _.forEach(error.response.data.errors, error => {
    96.                 arr = _.concat(arr, error.messages);
    97.             });
    98.  
    99.             result.details = arr;
    100.         }
    101.     } else {
    102.         result.name = error.name;
    103.         result.message = error.message;
    104.     }
    105.  
    106.     throw new Error(JSON.stringify(result));
    107. }
    108.  
     
  5. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    Thanks for highlighting those, we'll look into updating the documentation to improve it with this feedback!

    As for your other question on running directly your script locally, it is indeed more difficult to do so at the moment especially when interacting with other services like you are doing because of the required context (the accesstoken for a specific player, etc...) and simulate those without the server context.

    Unfortunately, for the moment, publishing your script in a test environment and running it would be a way of working through it.

    The other option with Jest in terms of UnitTest setup would imply mocking the server call, though I am not sure this is what you are looking for in this particular case. If you do want to look into that option, let us know, we can share what it could look like for your script.
     
  6. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    It's exactly what I am looking for. Would be great if you help me to create Jest unit test with mocking server calls. For me right now JS development its just a lot of trials and errors, and creating test and run them locally would greatly speed up my development
     
  7. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    And one more thing;
    Feedback: When i run scripts in unity dashboard, script run using test user. But in my case test users dose not have CloudData, therefore i cant realy test script
     
  8. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    Oh I see, thanks for the clarification! Here's what you could try out:

    - In your package.json (the one that is at the same level as the Asset folder), for reference it should start with :
    Code (JavaScript):
    1. {
    2.   "name": "your-project",
    3.   "version": "1.0.0",
    4.   "main": "index.js",
    5.   "scripts": {
    6.     "test": "jest"
    7.   },
    8.   "keywords": [],
    9.   "author": "",
    Make sure that the script element is:
    Code (JavaScript):
    1. "scripts": {"test": "jest"}
    and also that the "jest" element is:
    Code (JavaScript):
    1.  
    2. "jest": {
    3.     "moduleFileExtensions": [
    4.         "es10",
    5.         "js"
    6.     ],
    7.     "testMatch": [
    8.         "**/*.test.es10",
    9.         "**/*.test.js"
    10.     ]
    11. }
    12.  
    - Create a new file, if you script is called inventory.js => the new file could be inventory.test.js.
    Here's the proposed content

    Code (JavaScript):
    1.  
    2. const { InventoryApi } = require("@unity-services/economy-2.4");
    3. const { DataApi } = require("@unity-services/cloud-save-1.2");
    4.  
    5. jest.mock("@unity-services/economy-2.4", () => ({ InventoryApi: jest.fn() }));
    6. jest.mock("@unity-services/cloud-save-1.2", () => ({ DataApi: jest.fn() }));
    7.  
    8. const getInventory = require("./inventory.js");
    9.  
    10. const mockTavern = {
    11.     heroesInTavern: ["bd17d622-54e4-4378-85e3-4ff32be1b0c7"],
    12.     level: 1,
    13.     slotsCount: 2,
    14. };
    15. const heroData = {
    16.     Name: "mockedName",
    17.     attributes: [
    18.         { Name: "Strength", Value: 1 },
    19.         { Name: "Perception", Value: 1 },
    20.         { Name: "Endurance", Value: 1 },
    21.         { Name: "Charisma", Value: 1 },
    22.         { Name: "Intelligence", Value: 1 },
    23.         { Name: "Agility", Value: 1 },
    24.         { Name: "Luck", Value: 1 },
    25.     ],
    26. };
    27. const addInventoryRequest = {
    28.     inventoryItemId: "HERO",
    29.     instanceData: heroData,
    30. };
    31. const mockAddInventoryResponse = {
    32.     playersInventoryItemId: "mock-guid",
    33.     inventoryItemId: "HERO",
    34.     instanceData: heroData,
    35. };
    36.  
    37. describe("inventory tests", () => {
    38.     let mockDataApi, mockInventoryApi;
    39.  
    40.     beforeEach(() => {
    41.         mockDataApi = {
    42.             getItems: jest.fn(() => ({
    43.                 data: { results: [{ value: mockTavern }] },
    44.             })),
    45.             setItem: jest.fn(() => ({})),
    46.         };
    47.         mockInventoryApi = {
    48.             addInventoryItem: jest.fn(() => ({
    49.                 data: mockAddInventoryResponse,
    50.             })),
    51.         };
    52.  
    53.         DataApi.mockReturnValue(mockDataApi);
    54.         InventoryApi.mockReturnValue(mockInventoryApi);
    55.     });
    56.  
    57.     it("testGetInventory", async () => {
    58.         await getInventory({
    59.             params: {
    60.                 name: "mockHeroName",
    61.                 slotId: 1,
    62.             },
    63.             context: {
    64.                 projectId: "",
    65.                 playerId: "",
    66.                 accesssToken: "",
    67.             },
    68.             logger: {
    69.                 info: console.log,
    70.             },
    71.         });
    72.  
    73.         expect(mockDataApi.getItems).toHaveBeenCalled();
    74.         expect(mockDataApi.setItem).toHaveBeenCalled();
    75.         expect(mockInventoryApi.addInventoryItem).toHaveBeenCalled();
    76.     });
    77. });
    78.  
    Note that refers to what your script is called, so adjust in consequence the line:
    Code (JavaScript):
    1. const getInventory = require("./inventory.js");
    And finally for VS Code, you can look up the "Jest Runner" extension, it will allow you to trigger debug on the "describe" test line. You can also run the test from the command-line: npm run test.

    Let me know if you have any issues with this, hope it help!
     
    Last edited: Feb 8, 2023
  9. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    1 - Thank you very much for help
    2 - For other who read this. If you have problem with eslit with jest. You need to update .eslintrc.json
    need add "jest" : true
    Code (JavaScript):
    1. {
    2.     "env": {
    3.         "browser": true,
    4.         "commonjs": true,
    5.         "es2021": true,
    6.         "jest" : true
    7.     },
    8.     "extends": "eslint:recommended",
    9.     "overrides": [
    10.     ],
    11.     "parserOptions": {
    12.         "ecmaVersion": "latest"
    13.     },
    14.     "rules": {
    15.     }
    16. }
    17.  
    3 - I have problem with test
    Code (JavaScript):
    1.  FAIL  Assets/Scripts/CloudCodeTests/Tavern_GenerateHero.test.js
    2.   ● Test suite failed to run
    3.  
    4.     Cannot find module '@unity-services/economy-2.4' from 'Assets/Scripts/CloudCodeTests/Tavern_GenerateHero.test.js'
    5.  
    6.       3 | const { DataApi } = require("@unity-services/cloud-save-1.2");
    7.       4 |
    8.     > 5 | jest.mock("@unity-services/economy-2.4", () => ({ InventoryApi: jest.fn() }));
    9.         |           ^
    10.       6 | jest.mock("@unity-services/cloud-save-1.2", () => ({ DataApi: jest.fn() }));
    11.       7 |
    12.       8 | const getInventory = require("..CloudCode/Taver_GenerateHero");
    13.  
    14.       at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11)
    15.       at Object.<anonymous> (Assets/Scripts/CloudCodeTests/Tavern_GenerateHero.test.js:5:11)
     
    gpaquin-unity likes this.
  10. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    Hi! thanks for the update and sharing additional tips here :)

    For the failure you have, I think it is because your script was referencing the latest library for economy/cloud save that were not already embeded in the package. We just updated it last week, so perhaps you could try updating Cloud Code to 2.2.4 and test with it.
     
    ViliamVolosV likes this.
  11. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    1 - I hade to manualy add "com.unity.services.cloudcode": "2.2.4", in manifest json (no choise in Asset Manager , Unity 2021.3.16)
    2 - To update packages for node i press "Initialize JS Project" again - i think it should be automated. When user update CC modele and JS project already init, CC module should be update package.json
    3 - and after that ITS WORK
     
    gpaquin-unity likes this.
  12. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    I have other guestion.
    Is there any way to use my JS library in CC js scripts.
    As far i understand i cannot upload my library to cloud. maybe there is another way.
    For example - in CC examples you guys have code
    Code (JavaScript):
    1. // Some form of this function appears in all Cloud Code scripts.
    2. // Its purpose is to parse the errors thrown from the script into a standard exception object which can be stringified.
    3. function transformAndThrowCaughtError(error) {
    4.     let result = {
    5.         status: 0,
    6.         name: "",
    7.         message: "",
    8.         retryAfter: null,
    9.         details: ""
    10.     };
    11.  
    12.     if (error.response) {
    13.         result.status = error.response.data.status ? error.response.data.status : 0;
    14.         result.name = error.response.data.title ? error.response.data.title : "Unknown Error";
    15.         result.message = error.response.data.detail ? error.response.data.detail : error.response.data;
    16.  
    17.         if (error.response.status === tooManyRequestsError) {
    18.             result.retryAfter = error.response.headers['retry-after'];
    19.         } else if (error.response.status === badRequestError) {
    20.             let arr = [];
    21.  
    22.             _.forEach(error.response.data.errors, error => {
    23.                 arr = _.concat(arr, error.messages);
    24.             });
    25.  
    26.             result.details = arr;
    27.         }
    28.     } else {
    29.         result.name = error.name;
    30.         result.message = error.message;
    31.     }
    32.  
    33.     throw new Error(JSON.stringify(result));
    34. }
    is that any way to create my module\libraly for example with function transformAndThrowCaughtError.
    And than before i upload JS script to cloud, i some how "compile" this scripts and all methods that i use from my lib
    just automatically adds to this script
     
  13. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    Thanks for the update! Quick info:
    1 - Indeed I should have clarified, this is a very recent release of the package (last week). It should be available from the package list in the coming days/weeks.
    2 - This is a good point, definitely a good room for improvements, I'll bring this feedback to the team!

    We indeed don't have a good solution at the moment, but we do have something in the works around this. Will share more once we have the updates ready.
     
    ViliamVolosV likes this.
  14. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    If you want i can join for some kind of beta test if you have it
     
    gpaquin-unity likes this.
  15. ViliamVolosV

    ViliamVolosV

    Joined:
    Feb 12, 2013
    Posts:
    19
    I have other question about mocking data, if i may.
    How do i mock cloudSaveApi.getItems with different params.
    Example
    Code (JavaScript):
    1. const tavernDataKey = "TavernData";
    2. const barracsDataKey = "BarracksData";
    3.  
    4.         const getTavernData = await cloudSaveApi.getItems(projectId, playerId, [ tavernDataKey ] );
    5.         const getBarracsData = await cloudSaveApi.getItems(projectId, playerId, [ barracsDataKey ] );
    6.  
    7.  
    8. and this is example that you gave me last time that mock getitems
    9.  
    10.     beforeEach(() => {
    11.         mockDataApi = {
    12.             getItems: jest.fn(() => ({
    13.                 data: { results: [{ value: mockTavern }] },
    14.             })),
    15.             setItem: jest.fn(() => ({})),
    16.         };
    17.         mockInventoryApi = {
    18.             addInventoryItem: jest.fn(() => ({
    19.                 data: mockAddInventoryResponse,
    20.             })),
    21.         };
    22.  
    23.         DataApi.mockReturnValue(mockDataApi);
    24.         InventoryApi.mockReturnValue(mockInventoryApi);
    25.     });
     
  16. gpaquin-unity

    gpaquin-unity

    Unity Technologies

    Joined:
    Dec 13, 2019
    Posts:
    41
    The "jest.fn()" can be replicated more closely with the input params and then manage the return logic from there. Here is a rough example of what I mean:

    Code (JavaScript):
    1.  
    2. mockDataApi = {
    3.     getItems: jest.fn((projectId, playerId, dataKeyMap) =>
    4.     {
    5.         let result = [];
    6.  
    7.         if (dataKeyMap.includes(tavernDataKey))
    8.         {
    9.             result.push({ value: mockTavern});
    10.         }
    11.         if (dataKeyMap.includes(barrackDataKey))
    12.         {
    13.             result.push({ value: mockBarracks});
    14.         }              
    15.  
    16.         return ({data: { results: result }});
    17.     }),
    18. ...
    19.  
    Hopefully this will help you!
     
    ViliamVolosV likes this.