├── .env.template ├── .gitignore ├── LICENSE.md ├── README.md ├── archive-project-all.js ├── archive-project.js ├── config.template.json ├── cordova-publish.js ├── csp-patch.js ├── download-project.js ├── engine-patches ├── one-page-http-get.js ├── one-page-inline-game-scripts.js ├── one-page-mraid-resize-canvas.js └── one-page-no-xhr-request.js ├── library-files ├── lz4-browserify │ ├── main.js │ ├── package-lock.json │ ├── package.json │ └── reademe.md ├── lz4.js └── snapchat-cta.js ├── one-page.js ├── package-lock.json ├── package.json ├── shared.js └── tests ├── configs-one-page ├── config.json.cubejump-mraid-interstitial.json ├── config.json.cubejump-playable-fb.json ├── config.json.cubejump-snapchat-ad.json ├── config.json.fileadaudit-mraid-interstitial.json ├── config.json.flappy-compressed-engine.json ├── config.json.flappy-mraid-interstitial.json ├── config.json.flappy-playable-fb.json ├── config.json.flappy.json └── config.json.xwing-extern-files.json └── test-one-page.js /.env.template: -------------------------------------------------------------------------------- 1 | AUTH_TOKEN=abcd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | temp/ 3 | node_modules/ 4 | .DS_Store 5 | config.json 6 | *.idea 7 | .zed 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2021 PlayCanvas Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playcanvas-rest-api-tools 2 | 3 | This is a repo with a setup of tools to handle some of the more common needs of users with the REST API. 4 | 5 | Currently they are: 6 | 7 | * Downloading a build to self host 8 | * Downloading a build and add Content Security Policy (CSP) rules 9 | * Archiving a project for offline backup and importing a branch into a new project 10 | * Archiving all branches in a project for backup 11 | 12 | All downloaded files can be found in `temp/out`. 13 | 14 | ## Requirements 15 | Install [Node JS (v20+)](https://nodejs.org/en/download/) 16 | 17 | ## Setup 18 | 1. Clone this repo 19 | 2. `mv .env.template .env` or make a of copy the `.env.template` file and rename to `.env` and add your PlayCanvas Auth Token in there 20 | 3. `mv config.template.json config.json` or make a of copy the `config.template.json` file and rename to `config.json` and add your configuration in there (Project name, branch, scenes, CSP rules, etc. The parameters for the PlayCanvas object are explained in the [User Manual](https://developer.playcanvas.com/en/user-manual/api/)). 21 | 4. `npm install` 22 | 23 | --- 24 | 25 | ## Downloading a build 26 | 27 | This uses the [Download App REST API](https://developer.playcanvas.com/en/user-manual/api/app-download/) to download a build from your project to self host. 28 | 29 | ### Usage 30 | 1. `npm run download` 31 | 32 | #### Example 33 | ``` 34 | $ npm run download 35 | ✔️ Requested build from Playcanvas 36 | ↪️ Polling job 99999 37 | job still running 38 | will wait 1s and then retry 39 | ↪️ Polling job 99999 40 | ✔️ Job complete! 41 | ✔ Downloading zip https://somefilename.zip 42 | Success somefilename_Download.zip 43 | ``` 44 | 45 | ## Downloading a build and add CSP rules 46 | 47 | This uses the [Download App REST API](https://developer.playcanvas.com/en/user-manual/api/app-download/) to download a build from your project to self host. 48 | 49 | It will unzip the build, add the [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) rules to the `index.html` file and rezip the project. 50 | 51 | Please configure the CSP lists in `config.json` under `csp`. There is an option to also patch the preload bundles. To do so, set `patch_preload_bundles` to be true. 52 | 53 | ### Usage 54 | 1. `npm run csp` 55 | 56 | #### Example 57 | ``` 58 | $ npm run csp 59 | ✔️ Requested build from Playcanvas 60 | ↪️ Polling job 99999 61 | job still running 62 | will wait 1s and then retry 63 | ↪️ Polling job 99999 64 | ✔️ Job complete! 65 | ✔ Downloading zip https://somefilename.zip 66 | ✔️ Adding CSP 67 | ✔️ Zipping it all back again 68 | ✔️... Done! somefilename_WithCSP.zip 69 | ``` 70 | 71 | ## Archiving a project 72 | This uses the [Archive Project REST API](https://developer.playcanvas.com/en/user-manual/api/project-archive/) to archive a single branch that can be imported into a new project on PlayCanvas. 73 | 74 | ### Usage 75 | 1. `npm run archive` 76 | 77 | #### Example 78 | ``` 79 | $ npm run archive 80 | ✔️ Requested archive from Playcanvas 81 | ↪️ Polling job 99999 82 | job still running 83 | will wait 1s and then retry 84 | ↪️ Polling job 99999 85 | ✔️ Job complete! 86 | ✔ Downloading zip https://somefilename.zip 87 | Success somefilename_Download.zip 88 | ``` 89 | 90 | ## Archiving all branches in a project 91 | 92 | This uses the [Archive Project](https://developer.playcanvas.com/en/user-manual/api/project-archive/) and [List Branches](https://developer.playcanvas.com/en/user-manual/api/branch-list/) REST APIs to download all open branches in a project. 93 | 94 | As the API is [strict limited](https://developer.playcanvas.com/en/user-manual/api/#rate-limiting), it is a slow job and may take a while to complete if you have a lot of branches. 95 | 96 | ### Usage 97 | 1. `npm run archive-all` 98 | 99 | #### Example 100 | ``` 101 | $ npm run archive-all 102 | ✔️ Requested branch list from Playcanvas 103 | ↪️ Processing branch list from Playcanvas 104 | ↪️ Start archiving all 2 branches... 105 | ↪️ 1 of 2 branches: b1 106 | ✔️ Requested archive from Playcanvas 107 | ↪️ Polling job 99999 108 | job still running 109 | will wait 1s and then retry 110 | ↪️ Polling job 99999 111 | ✔️ Job complete! 112 | ✔ Downloading zip https://somefilename.zip 113 | ↪️ 2 of 2 branches: b10 114 | ✔️ Requested archive from Playcanvas 115 | ↪️ Polling job 99999 116 | job still running 117 | will wait 1s and then retry 118 | ↪️ Polling job 99999 119 | ✔️ Job complete! 120 | ✔ Downloading zip hhttps://somefilename.zip 121 | Success 122 | ``` 123 | 124 | ## Converting a project into a single HTML file 125 | 126 | This uses the [Download App REST API](https://developer.playcanvas.com/en/user-manual/api/app-download/) to download a build from your project to self host. 127 | 128 | The script will then unzip the project, convert assets, scripts, etc into Base64 and embed them into the index.html with the intention to be used for some playable ads formats. 129 | 130 | Once finished, it will copy the HTML file to the out folder. 131 | 132 | There are some limitations: 133 | - Modules are not supported (Basis and Ammo) 134 | - Texture compression formats are not supported 135 | - Asset Bundles are not supported 136 | - ~~Spine runtime is not supported~~ Now supported since since [PR#42](https://github.com/playcanvas/playcanvas-spine/commit/77514b0bc6a5c87263d6225f10eb011096ceed2d) 137 | - Any code relying on asset URLs being a file path will not work as they will be Base64 encoded 138 | 139 | As Ammo is not supported for physics, alternatives are: 140 | - cannon.js for 3D physics ([PlayCanvas integration here](https://playcanvas.com/project/793652/overview/cannon-physics-basic-integration)) 141 | - p2.js for 2D physics ([PlayCanvas integration here](https://playcanvas.com/project/446127/overview/p2js-integration)) 142 | - Using PlayCanvas [Bounding Sphere](https://developer.playcanvas.com/en/api/pc.BoundingSphere.html), [Bounding Box](https://developer.playcanvas.com/en/api/pc.BoundingBox.html), [Orientated Box](https://developer.playcanvas.com/en/api/pc.OrientedBox.html) for simple overlap checking and raycasting 143 | 144 | ### Experimental features 145 | 146 | #### Remove XHR requests 147 | Adds an engine patch to remove any XHR requests and decodes the base64 URLs directly. This may be required for some platforms where this is not permitted. As this is a patch, there may be edge cases where some asset types may not work. If you find any any, please report them in the issues. 148 | 149 | The option can be found in `config.json` under `one_page`. Set `patch_xhr_out` to true. 150 | 151 | #### Inline game scripts 152 | Adds an engine patch to decode base64 URLS for JS scripts when the engine adds them to the page document. This may be required for some platforms that block base64 encoded JS URLs. As this is a patch, there may be edge cases where some asset types may not work. If you find any, please report them in the issues. 153 | 154 | The option can be found in `config.json` under `one_page`. Set `inline_game_scripts` to true. 155 | 156 | #### Extern files 157 | Enabling this will keep the PlayCanvas engine code and game data as separate files. It will also zip up these files as the output file. This can be used for platforms that have a larger allowance for a zipped package to be used compared to a single HTML file. 158 | 159 | The option can be found in `config.json` under `one_page`. Set `extern_files.enabled` to true. The files can also be in a separate folder using `extern_files.folder_name` (defaults to the same directory). 160 | 161 | In some cases with ad networks, the external files will need to be hosted elsewhere such as a CDN. `extern_files.external_url_prefix` can be used to have the `index.html` reference the files to the CDN. E.g. 162 | 163 | ``` 164 | "extern_files": { 165 | "enabled": true, 166 | "folder_name": "78fb9255-3033-4fe2-b9e1-355b149229a1", 167 | "external_url_prefix": "https://some/random/cdn" 168 | } 169 | ``` 170 | 171 | #### MRAID interstitial support 172 | 173 | Adds basic support for MRAID API within the PlayCanvas engine and boilerplate code. 174 | 175 | The option can be found in `config.json` under `one_page`. Set `mraid_support` to true. 176 | 177 | #### Snapchat ad support 178 | 179 | The Snapchat ad network requires the CTA function to be in the `index.html` where the network can replace it with a unique tracking version when it is served to the user. The URL will be set in the Snapchat Ad campaign tool. 180 | 181 | The ad project should call `snapchatCta();` as the CTA function instead of `mraid.open('someurl');`. 182 | 183 | The option can be found in `config.json` under `one_page`. Set `snapchat_cta` to true. 184 | 185 | #### Compress engine code 186 | 187 | Compresses the engine file to save 500KB on the final file size, leaving more room for games assets. Especially with playable ad networks only allowing 2MB for a single HTML file. 188 | 189 | This should only be used if you need the extra space as it adds extra initialisation time to decompress the engine code at runtime. Benchmarks below: 190 | 191 | - Google Pixel 2XL: ~180ms 192 | - Samsung Galaxy S7: ~180ms 193 | 194 | The option can be found in `config.json` under `one_page`. Set `compress_engine` to true. 195 | 196 | ### Usage 197 | 1. `npm run one-page` 198 | 199 | #### Example 200 | ``` 201 | $ npm run one-page 202 | ✔️ Requested build from Playcanvas 203 | ↪️ Polling job 710439 204 | job still running 205 | will wait 1s and then retry 206 | ↪️ Polling job 710439 207 | ✔️ Job complete! 208 | ✔ Downloading zip someBuild.zip 209 | ✔️ Unzipping someBuild.zip 210 | ↪️ Removing manifest.json 211 | ↪️ Removing __modules__.js 212 | ↪️ Inlining style.css into index.html 213 | ↪️ Base64 encode all urls in config.json 214 | ↪️ Remove __loading__.js 215 | ↪️ Base64 encode the scene JSON and config JSON files 216 | ↪️ Patching __start__.js 217 | ↪️ Inline JS scripts in index.html 218 | ✔️ Finishing up 219 | Success someProject.html 220 | ``` 221 | 222 | #### Testing 223 | Please use the following command to create the most common outputs for the one-page job with public projects owned by the PlayCanvas team. 224 | 225 | 1. `npm run test-one-page` 226 | 227 | ### Asset Size Report 228 | To get a size report of the one-page job, use the following command: 229 | 1. `npm run one-page --size-report` 230 | #### Example 231 | ``` 232 | Size Report 233 | Character.glb - 311472 bytes 234 | Character.png - 165976 bytes 235 | MaterialShader.js - 14980 bytes 236 | Fire-Noise.jpg - 6696 bytes 237 | Sound.mp3 - 1724 bytes 238 | Total size: 500848 bytes 239 | Total asset size in MB: 0.48 MB 240 | ``` 241 | 242 | ## Cordova Publish 243 | 244 | This uses the [Download App REST API](https://developer.playcanvas.com/en/user-manual/api/app-download/) to download a build and also prepare it to be used with Cordova to create a native app. 245 | 246 | Currently, it does the following actions: 247 | * Adds `cordova.js` as a script header in `index.html` 248 | * Converts all audio assets to Base64 so they can be loaded on iOS 249 | 250 | ### Usage 251 | 1. `npm run cordova-publish` 252 | 253 | #### Example 254 | ``` 255 | $ npm run cordova-publish 256 | ✔️ Requested build from Playcanvas 257 | ↪️ Polling job 858473 258 | job still running 259 | will wait 1s and then retry 260 | ↪️ Polling job 858473 261 | ✔️ Job complete! 262 | ✔ Downloading zip https://somefile.zip 263 | ✔️ Unzipping /somefile_Download.zip 264 | ↪️ Base64 encode audio assets config.json 265 | ✔️ Zipping it all back again 266 | ``` 267 | -------------------------------------------------------------------------------- /archive-project-all.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const shared = require('./shared'); 6 | 7 | function getBranches(config) { 8 | return new Promise((resolve, reject) => { 9 | console.log("✔️ Requested branch list from Playcanvas"); 10 | let url = 'https://playcanvas.com/api/projects/' + config.playcanvas.project_id + '/branches'; 11 | 12 | fetch(url, { 13 | method: 'GET', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | 'Authorization': 'Bearer ' + config.authToken 17 | } 18 | }) 19 | .then(res => { 20 | if (res.status !== 200) { 21 | throw new Error("Error: status code " + res.status); 22 | } 23 | return res.json(); 24 | }) 25 | .then(branches => { 26 | resolve(branches); 27 | }) 28 | .catch(reject); 29 | }); 30 | } 31 | 32 | function processBranches (branches) { 33 | return new Promise((resolve, reject) => { 34 | console.log("↪️ Processing branch list from Playcanvas"); 35 | 36 | let branchData = []; 37 | 38 | let results = branches.result; 39 | for (let i = 0; i < results.length; i++) { 40 | let result = results[i]; 41 | branchData.push({ 42 | name: result.name.replace(/[^a-z0-9]/gi, '_').toLowerCase(), 43 | id: result.id 44 | }); 45 | } 46 | 47 | resolve(branchData); 48 | }); 49 | } 50 | 51 | async function archiveBranches(config, branchData) { 52 | console.log("↪️ Start archiving all " + branchData.length + " branches..."); 53 | 54 | // Stict rate limit is 5 requests a miniute so we will keep track of this 55 | // and wait when need after 5 jobs 56 | const maxJobs = 5; 57 | const durationMs = 60 * 1000; 58 | let startTime = Date.now(); 59 | let currentJobCount = 0; 60 | 61 | for (let i = 0; i < branchData.length; i++) { 62 | let branch = branchData[i]; 63 | console.log("↪️ " + (i+1) + " of " + branchData.length + " branches: " + branch.name); 64 | await shared.archiveProject(config, branch.name, branch.id, "temp/out"); 65 | 66 | currentJobCount++; 67 | 68 | if (currentJobCount === maxJobs) { 69 | // Make sure we don't go other the strict rate limit 70 | let jobDurationMs = (Date.now() - startTime); 71 | 72 | if (jobDurationMs < durationMs) { 73 | console.log("↪️ Waiting " + Math.floor(jobDurationMs / 1000) + "s to stay within API rate limits..."); 74 | await shared.sleep(durationMs - jobDurationMs); 75 | } 76 | 77 | startTime = Date.now(); 78 | currentJobCount = 0; 79 | } 80 | } 81 | } 82 | 83 | const config = shared.readConfig(); 84 | 85 | getBranches(config) 86 | .then(processBranches) 87 | .then((branchData) => archiveBranches(config, branchData)) 88 | .then(() => console.log("Success")) 89 | .catch(err => console.log("Error", err)); 90 | 91 | -------------------------------------------------------------------------------- /archive-project.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const shared = require('./shared'); 6 | 7 | const config = shared.readConfig(); 8 | 9 | shared.archiveProject(config, config.playcanvas.branch_name, config.playcanvas.branch_id, "temp/out") 10 | .then((output) => console.log("Success", output)) 11 | .catch(err => console.log("Error", err)); 12 | 13 | -------------------------------------------------------------------------------- /config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 9999, 4 | "name": "Project name", 5 | "scenes" : [9999], 6 | "branch_id": "99999999-9999-9999-9999-999999999999", 7 | "description": "", 8 | "preload_bundle": false, 9 | "version": "", 10 | "release_notes": "", 11 | "scripts_concatenate": true, 12 | "scripts_sourcemaps": false, 13 | "scripts_minify": true, 14 | "optimize_scene_format": false, 15 | "engine_version": "" 16 | }, 17 | "csp": { 18 | "style-src": [ 19 | "'self'", 20 | "'unsafe-inline'" 21 | ], 22 | "connect-src": [ 23 | "'self'", 24 | "blob:", 25 | "data:", 26 | "https://*.google-analytics.com" 27 | ], 28 | "patch_preload_bundles": false 29 | }, 30 | "one_page": { 31 | "patch_xhr_out": false, 32 | "inline_game_scripts": false, 33 | "extern_files": { 34 | "enabled": false, 35 | "folder_name": "", 36 | "external_url_prefix": "" 37 | }, 38 | "mraid_support": false, 39 | "snapchat_cta": false, 40 | "compress_engine": false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cordova-publish.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const base64js = require('base64-js'); 4 | const { minify } = require('terser'); 5 | const btoa = require('btoa'); 6 | const replaceString = require('replace-string'); 7 | const lz4 = require('lz4'); 8 | 9 | const shared = require('./shared'); 10 | 11 | const config = shared.readConfig(); 12 | 13 | var externFiles = [ 14 | ]; 15 | 16 | function patch(projectPath) { 17 | return new Promise((resolve, reject) => { 18 | (async function() { 19 | let indexLocation = path.resolve(projectPath, "index.html"); 20 | let indexContents = fs.readFileSync(indexLocation, 'utf-8'); 21 | 22 | indexContents = indexContents.replace( 23 | '', 24 | '\n ' 25 | ); 26 | 27 | // Open config.json and replace urls of audio assets with base64 strings of the files with 28 | // the correct mime type. Also, delete the audio files to save on package size 29 | await (async function() { 30 | console.log("↪️ Base64 encode audio assets config.json"); 31 | 32 | let location = path.resolve(projectPath, "config.json"); 33 | let contents = fs.readFileSync(location, 'utf-8'); 34 | 35 | let configJson = JSON.parse(contents); 36 | let assets = configJson.assets; 37 | 38 | for (const [key, asset] of Object.entries(assets)) { 39 | if (!Object.prototype.hasOwnProperty.call(assets, key)) { 40 | continue; 41 | } 42 | 43 | // If it's not a file or an audio asset, we can ignore 44 | if (!asset.file || asset.type !== 'audio') { 45 | continue; 46 | } 47 | 48 | let url = unescape(asset.file.url); 49 | let urlSplit = url.split('.'); 50 | let extension = urlSplit[urlSplit.length - 1]; 51 | 52 | let filepath = path.resolve(projectPath, url); 53 | if (!fs.existsSync(filepath)) { 54 | console.log(" Cannot find file " + filepath + " If it's a loading screen script, please ignore"); 55 | continue; 56 | } 57 | 58 | let fileContents = fs.readFileSync(filepath); 59 | let mimeprefix = "data:application/octet-stream"; 60 | 61 | let ba = Uint8Array.from(fileContents); 62 | let b64 = base64js.fromByteArray(ba); 63 | 64 | // As we are using an escaped URL, we will search using the original URL 65 | asset.file.url = mimeprefix + ';base64,' + b64; 66 | 67 | // Remove the hash to prevent appending to the URL 68 | asset.file.hash = ""; 69 | 70 | // Delete the audio file 71 | fs.unlinkSync(filepath); 72 | }; 73 | 74 | fs.writeFileSync(location, JSON.stringify(configJson)); 75 | })(); 76 | 77 | fs.writeFileSync(indexLocation, indexContents); 78 | resolve(projectPath); 79 | })(); 80 | }); 81 | } 82 | 83 | shared.downloadProject(config, "temp/downloads") 84 | .then((zipLocation) => shared.unzipProject(zipLocation, 'contents') ) 85 | .then(patch) 86 | .then((rootFolder) => shared.zipProject(rootFolder, 'temp/out/'+config.playcanvas.name+'_CordovaPublish.zip')) 87 | .then(outputZip => console.log("Success", outputZip)) 88 | .catch(err => console.log("Error", err)); 89 | -------------------------------------------------------------------------------- /csp-patch.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const shared = require('./shared'); 5 | 6 | const config = shared.readConfig(); 7 | 8 | 9 | function updatePreloadBundles(rootFolder) { 10 | return new Promise((resolve) => { 11 | (async function() { 12 | if (config.csp.patch_preload_bundles) { 13 | // Check if the preload bundles exist 14 | const updateBundle = async function (bundleName) { 15 | if (fs.existsSync(bundleName)) { 16 | console.log("↪️ Updating " + path.basename(bundleName)); 17 | 18 | const tempFolder = await shared.unzipProject(bundleName, 'bundle-temp'); 19 | await addCspMetadata(tempFolder); 20 | await shared.zipProject(tempFolder, bundleName); 21 | } 22 | } 23 | 24 | await updateBundle(rootFolder + '/preload-android.zip'); 25 | await updateBundle(rootFolder + '/preload-ios.zip'); 26 | } 27 | resolve(rootFolder); 28 | })(); 29 | }); 30 | } 31 | 32 | function addCspMetadata(projectLocation) { 33 | return new Promise((resolve, reject) => { 34 | console.log("✔️ Adding CSP"); 35 | var indexLocation = path.resolve(projectLocation, 'index.html'); 36 | var indexContents = fs.readFileSync(indexLocation, 'utf-8'); 37 | var headStart = indexContents.indexOf(""); 38 | if (headStart < 0) { 39 | reject(new Error('Could not find head tag in index.html', indexLocation, indexContents)); 40 | } else { 41 | var cspMetadata = getCspMetadataTag(); 42 | var indexWithCsp = indexContents.replace("", "\n\t"+cspMetadata); 43 | fs.writeFileSync(indexLocation, indexWithCsp); 44 | resolve(path.dirname(indexLocation)); 45 | } 46 | }); 47 | } 48 | 49 | function getCspMetadataTag() { 50 | var tag = "" 51 | var content = ""; 52 | for (var key in config.csp) { 53 | if (key !== 'patch_preload_bundles') { 54 | content += key; 55 | for (var i in config.csp[key]) { 56 | var value = config.csp[key][i]; 57 | content += " " + value 58 | } 59 | content += "; " 60 | } 61 | } 62 | 63 | return tag.replace("{0}", content); 64 | } 65 | 66 | // Unzip the build and change the index.html CSP 67 | // If there are preload bundles, then the index.html in the 68 | // bundles will need to be modified too 69 | 70 | shared.downloadProject(config, "temp/downloads") 71 | .then((zipLocation) => shared.unzipProject(zipLocation, "contents/")) 72 | .then(addCspMetadata) 73 | .then(updatePreloadBundles) 74 | .then((rootFolder) => shared.zipProject(rootFolder, 'temp/out/'+config.playcanvas.name+'_WithCSP.zip')) 75 | .then(outputZip => console.log("Success", outputZip)) 76 | .catch(err => console.log("Error", err)); 77 | -------------------------------------------------------------------------------- /download-project.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const shared = require('./shared'); 6 | 7 | const config = shared.readConfig(); 8 | 9 | shared.downloadProject(config, "temp/out") 10 | .then((output) => console.log("Success", output)) 11 | .catch(err => console.log("Error", err)); 12 | 13 | -------------------------------------------------------------------------------- /engine-patches/one-page-http-get.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // We pass the config as an object instead of a Base64 string to save file size 3 | // This patches the http get function to return immediately if the URL is already an object 4 | var oldGet = pc.Http.prototype.get; 5 | 6 | pc.Http.prototype.get = function get(url, options, callback) { 7 | if (typeof options === "function") { 8 | callback = options; 9 | options = {}; 10 | } 11 | 12 | // If the url is an object, just return it 13 | if (typeof(url) === 'object') { 14 | callback(null, url); 15 | return; 16 | } 17 | 18 | oldGet.call(this, url, options, callback); 19 | } 20 | })(); 21 | -------------------------------------------------------------------------------- /engine-patches/one-page-inline-game-scripts.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | pc.ScriptHandler.prototype._loadScript = function (url, callback) { 3 | var head = document.head; 4 | var element = document.createElement('script'); 5 | this._cache[url] = element; 6 | 7 | // use async=false to force scripts to execute in order 8 | element.async = false; 9 | 10 | // Decode the url from base64 to text 11 | var index = url.indexOf(','); 12 | var base64 = url.slice(index + 1); 13 | var data = window.atob(base64); 14 | 15 | element.innerText = data; 16 | head.appendChild(element); 17 | 18 | callback(null, url, element); 19 | }; 20 | })(); -------------------------------------------------------------------------------- /engine-patches/one-page-mraid-resize-canvas.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | pc.Application.prototype.resizeCanvas = function (width, height) { 3 | if (!this._allowResize) return; // prevent resizing (e.g. if presenting in VR HMD) 4 | 5 | // prevent resizing when in XR session 6 | if (this.xr && this.xr.session) 7 | return; 8 | 9 | var windowWidth = window.innerWidth; 10 | var windowHeight = window.innerHeight; 11 | 12 | if (window.mraid) { 13 | var mraidSize = mraid.getMaxSize(); 14 | windowWidth = mraidSize.width; 15 | windowHeight = mraidSize.height; 16 | } 17 | 18 | if (this._fillMode === pc.FILLMODE_KEEP_ASPECT) { 19 | var r = this.graphicsDevice.canvas.width / this.graphicsDevice.canvas.height; 20 | var winR = windowWidth / windowHeight; 21 | 22 | if (r > winR) { 23 | width = windowWidth; 24 | height = width / r; 25 | } else { 26 | height = windowHeight; 27 | width = height * r; 28 | } 29 | } else if (this._fillMode === pc.FILLMODE_FILL_WINDOW) { 30 | width = windowWidth; 31 | height = windowHeight; 32 | } 33 | // OTHERWISE: FILLMODE_NONE use width and height that are provided 34 | 35 | this.graphicsDevice.canvas.style.width = width + 'px'; 36 | this.graphicsDevice.canvas.style.height = height + 'px'; 37 | 38 | this.updateCanvasSize(); 39 | 40 | // return the final values calculated for width and height 41 | return { 42 | width: width, 43 | height: height 44 | }; 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /engine-patches/one-page-no-xhr-request.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Patch out supportsImageBitmap as that doesn't load some images when XHR is also patched out 3 | // We override the setting in configure before we load assets 4 | var oldAppConfigure = pc.Application.prototype.configure; 5 | pc.Application.prototype.configure = function (json, callback) { 6 | this.graphicsDevice.supportsImageBitmap = false; 7 | oldAppConfigure.call(this, json, callback); 8 | }; 9 | 10 | pc.Http.prototype.get = function get(url, options, callback) { 11 | if (typeof options === "function") { 12 | callback = options; 13 | options = {}; 14 | } 15 | 16 | var index = url.indexOf(','); 17 | var base64 = url.slice(index + 1); 18 | var data = window.atob(base64); 19 | 20 | if (url.startsWith('data:application/json') || options.responseType === pc.Http.ResponseType.JSON) { 21 | data = JSON.parse(data); 22 | } else if (url.startsWith('data:text/plain')) { 23 | // Do nothing 24 | } else { 25 | // Assume binary if not JSON 26 | var len = data.length; 27 | var bytes = new Uint8Array(len); 28 | for (var i = 0; i < len; i++) { 29 | bytes[i] = data.charCodeAt(i); 30 | } 31 | data = bytes.buffer; 32 | 33 | if (url.startsWith('data:image/')) { 34 | data = new Blob([data]); 35 | } 36 | } 37 | 38 | callback(null, data); 39 | } 40 | })(); 41 | -------------------------------------------------------------------------------- /library-files/lz4-browserify/main.js: -------------------------------------------------------------------------------- 1 | var lz4 = require("lz4js"); 2 | var Buffer = require("buffer").Buffer; 3 | 4 | window.lz4 = lz4; 5 | window.Buffer = Buffer; 6 | -------------------------------------------------------------------------------- /library-files/lz4-browserify/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lz4-browsify", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lz4-browsify", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "lz4js": "^0.2.0" 13 | } 14 | }, 15 | "node_modules/lz4js": { 16 | "version": "0.2.0", 17 | "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", 18 | "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library-files/lz4-browserify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lz4-browsify", 3 | "version": "1.0.0", 4 | "description": "Packages needed to build a browserify version of lz4js with Buffer support", 5 | "main": "main.js", 6 | "author": "support@playcanvas.com", 7 | "license": "MIT", 8 | "dependencies": { 9 | "lz4js": "^0.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /library-files/lz4-browserify/reademe.md: -------------------------------------------------------------------------------- 1 | # How to build the Browserify version of lz4js 2 | 3 | Requires Node.js v20. 4 | 5 | 1. Install NPM packages via `npm i` 6 | 2. Install Browserify globablly `npm install -g browserify` 7 | 3. Install Terser globally `npm i -g terser` 8 | 4. Build the minified bundle `browserify main.js | terser --compress > ../lz4.js` 9 | -------------------------------------------------------------------------------- /library-files/lz4.js: -------------------------------------------------------------------------------- 1 | !function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,(function(r){return o(e[i][1][r]||r)}),p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i>4&7;if(void 0===bsMap[bsIdx])throw new Error("invalid block size "+bsIdx);var maxBlockSize=bsMap[bsIdx];if(useContentSize)return util.readU64(src,sIndex);sIndex++;for(var maxSize=0;;){var blockSize=util.readU32(src,sIndex);if(sIndex+=4,maxSize+=2147483648&blockSize?blockSize&=2147483647:maxBlockSize,0===blockSize)return maxSize;useBlockSum&&(sIndex+=4),sIndex+=blockSize}},exports.makeBuffer=makeBuffer,exports.decompressBlock=function(src,dst,sIndex,sLength,dIndex){var mLength,mOffset,sEnd,n,i;for(sEnd=sIndex+sLength;sIndex>4;if(literalCount>0){if(15===literalCount)for(;literalCount+=src[sIndex],255===src[sIndex++];);for(n=sIndex+literalCount;sIndex=sEnd)break;if(mLength=15&token,mOffset=src[sIndex++]|src[sIndex++]<<8,15===mLength)for(;mLength+=src[sIndex],255===src[sIndex++];);for(n=(i=dIndex-mOffset)+(mLength+=4);i=13)for(var searchMatchCount=67;sIndex+4>>0;if(mIndex=hashTable[hash=(hash>>16^hash)>>>0&65535]-1,hashTable[hash]=sIndex+1,mIndex<0||sIndex-mIndex>>>16>0||util.readU32(src,mIndex)!==seq)sIndex+=searchMatchCount++>>6;else{for(searchMatchCount=67,literalCount=sIndex-mAnchor,mOffset=sIndex-mIndex,mIndex+=4,mLength=sIndex+=4;sIndex=15){for(dst[dIndex++]=240+token,n=literalCount-15;n>=255;n-=255)dst[dIndex++]=255;dst[dIndex++]=n}else dst[dIndex++]=(literalCount<<4)+token;for(var i=0;i>8,mLength>=15){for(n=mLength-15;n>=255;n-=255)dst[dIndex++]=255;dst[dIndex++]=n}mAnchor=sIndex}}if(0===mAnchor)return 0;if((literalCount=sEnd-mAnchor)>=15){for(dst[dIndex++]=240,n=literalCount-15;n>=255;n-=255)dst[dIndex++]=255;dst[dIndex++]=n}else dst[dIndex++]=literalCount<<4;for(sIndex=mAnchor;sIndex>4&7;if(void 0===bsMap[bsIdx])throw new Error("invalid block size");for(useContentSize&&(sIndex+=8),sIndex++;;){var compSize;if(compSize=util.readU32(src,sIndex),sIndex+=4,0===compSize)break;if(useBlockSum&&(sIndex+=4),0!=(2147483648&compSize)){compSize&=2147483647;for(var j=0;j>8,dIndex++;var maxBlockSize=bsMap[7],remaining=src.length,sIndex=0;for(!function(table){for(var i=0;i<65536;i++)hashTable[i]=0}();remaining>0;){var compSize,blockSize=remaining>maxBlockSize?maxBlockSize:remaining;if((compSize=exports.compressBlock(src,blockBuf,sIndex,blockSize,hashTable))>blockSize||0===compSize){util.writeU32(dst,dIndex,2147483648|blockSize),dIndex+=4;for(var z=sIndex+blockSize;sIndex>>19)+374761393+(a<<5)|0)+-744332180^a<<9)+-42973499+(a<<3)|0)^a>>>16|0},exports.readU64=function(b,n){var x=0;return x|=b[n++]<<0,x|=b[n++]<<8,x|=b[n++]<<16,x|=b[n++]<<24,x|=b[n++]<<32,x|=b[n++]<<40,x|=b[n++]<<48,x|=b[n++]<<56},exports.readU32=function(b,n){var x=0;return x|=b[n++]<<0,x|=b[n++]<<8,x|=b[n++]<<16,x|=b[n++]<<24},exports.writeU32=function(b,n,x){b[n++]=x>>0&255,b[n++]=x>>8&255,b[n++]=x>>16&255,b[n++]=x>>24&255},exports.imul=function(a,b){var al=65535&a,bl=65535&b;return al*bl+((a>>>16)*bl+al*(b>>>16)<<16)|0}},{}],4:[function(require,module,exports){var util=require("./util.js"),prime1=2654435761,prime2=2246822519,prime3=3266489917,prime4=668265263,prime5=374761393;function rotl32(x,r){return(x|=0)>>>(32-(r|=0)|0)|x<>>(32-r|0)|h<>>(s|=0)^h|0}function xxhapply(h,src,m0,s,m1){return rotmul32(util.imul(src,m0)+h,s,m1)}function xxh1(h,src,index){return rotmul32(h+util.imul(src[index],prime5),11,prime1)}function xxh4(h,src,index){return xxhapply(h,util.readU32(src,index),prime3,17,prime4)}function xxh16(h,src,index){return[xxhapply(h[0],util.readU32(src,index+0),prime2,13,prime1),xxhapply(h[1],util.readU32(src,index+4),prime2,13,prime1),xxhapply(h[2],util.readU32(src,index+8),prime2,13,prime1),xxhapply(h[3],util.readU32(src,index+12),prime2,13,prime1)]}exports.hash=function(seed,src,index,len){var h,l;if(l=len,len>=16){for(h=[seed+prime1+prime2,seed+prime2,seed,seed-prime1];len>=16;)h=xxh16(h,src,index),index+=16,len-=16;h=rotl32(h[0],1)+rotl32(h[1],7)+rotl32(h[2],12)+rotl32(h[3],18)+l}else h=seed+prime5+len>>>0;for(;len>=4;)h=xxh4(h,src,index),index+=4,len-=4;for(;len>0;)h=xxh1(h,src,index),index++,len--;return(h=shiftxor32(util.imul(shiftxor32(util.imul(shiftxor32(h,15),prime2),13),prime3),16))>>>0}},{"./util.js":3}],5:[function(require,module,exports){"use strict";exports.byteLength=function(b64){var lens=getLens(b64),validLen=lens[0],placeHoldersLen=lens[1];return 3*(validLen+placeHoldersLen)/4-placeHoldersLen},exports.toByteArray=function(b64){var tmp,i,lens=getLens(b64),validLen=lens[0],placeHoldersLen=lens[1],arr=new Arr(function(b64,validLen,placeHoldersLen){return 3*(validLen+placeHoldersLen)/4-placeHoldersLen}(0,validLen,placeHoldersLen)),curByte=0,len=placeHoldersLen>0?validLen-4:validLen;for(i=0;i>16&255,arr[curByte++]=tmp>>8&255,arr[curByte++]=255&tmp;2===placeHoldersLen&&(tmp=revLookup[b64.charCodeAt(i)]<<2|revLookup[b64.charCodeAt(i+1)]>>4,arr[curByte++]=255&tmp);1===placeHoldersLen&&(tmp=revLookup[b64.charCodeAt(i)]<<10|revLookup[b64.charCodeAt(i+1)]<<4|revLookup[b64.charCodeAt(i+2)]>>2,arr[curByte++]=tmp>>8&255,arr[curByte++]=255&tmp);return arr},exports.fromByteArray=function(uint8){for(var tmp,len=uint8.length,extraBytes=len%3,parts=[],i=0,len2=len-extraBytes;ilen2?len2:i+16383));1===extraBytes?(tmp=uint8[len-1],parts.push(lookup[tmp>>2]+lookup[tmp<<4&63]+"==")):2===extraBytes&&(tmp=(uint8[len-2]<<8)+uint8[len-1],parts.push(lookup[tmp>>10]+lookup[tmp>>4&63]+lookup[tmp<<2&63]+"="));return parts.join("")};for(var lookup=[],revLookup=[],Arr="undefined"!=typeof Uint8Array?Uint8Array:Array,code="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",i=0;i<64;++i)lookup[i]=code[i],revLookup[code.charCodeAt(i)]=i;function getLens(b64){var len=b64.length;if(len%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var validLen=b64.indexOf("=");return-1===validLen&&(validLen=len),[validLen,validLen===len?0:4-validLen%4]}function encodeChunk(uint8,start,end){for(var tmp,num,output=[],i=start;i>18&63]+lookup[num>>12&63]+lookup[num>>6&63]+lookup[63&num]);return output.join("")}revLookup["-".charCodeAt(0)]=62,revLookup["_".charCodeAt(0)]=63},{}],6:[function(require,module,exports){(function(Buffer){(function(){ 2 | /*! 3 | * The buffer module from node.js, for the browser. 4 | * 5 | * @author Feross Aboukhadijeh 6 | * @license MIT 7 | */ 8 | "use strict";var base64=require("base64-js"),ieee754=require("ieee754");exports.Buffer=Buffer,exports.SlowBuffer=function(length){+length!=length&&(length=0);return Buffer.alloc(+length)},exports.INSPECT_MAX_BYTES=50;var K_MAX_LENGTH=2147483647;function createBuffer(length){if(length>K_MAX_LENGTH)throw new RangeError('The value "'+length+'" is invalid for option "size"');var buf=new Uint8Array(length);return buf.__proto__=Buffer.prototype,buf}function Buffer(arg,encodingOrOffset,length){if("number"==typeof arg){if("string"==typeof encodingOrOffset)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(arg)}return from(arg,encodingOrOffset,length)}function from(value,encodingOrOffset,length){if("string"==typeof value)return function(string,encoding){"string"==typeof encoding&&""!==encoding||(encoding="utf8");if(!Buffer.isEncoding(encoding))throw new TypeError("Unknown encoding: "+encoding);var length=0|byteLength(string,encoding),buf=createBuffer(length),actual=buf.write(string,encoding);actual!==length&&(buf=buf.slice(0,actual));return buf}(value,encodingOrOffset);if(ArrayBuffer.isView(value))return fromArrayLike(value);if(null==value)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof value);if(isInstance(value,ArrayBuffer)||value&&isInstance(value.buffer,ArrayBuffer))return function(array,byteOffset,length){if(byteOffset<0||array.byteLength=K_MAX_LENGTH)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+K_MAX_LENGTH.toString(16)+" bytes");return 0|length}function byteLength(string,encoding){if(Buffer.isBuffer(string))return string.length;if(ArrayBuffer.isView(string)||isInstance(string,ArrayBuffer))return string.byteLength;if("string"!=typeof string)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof string);var len=string.length,mustMatch=arguments.length>2&&!0===arguments[2];if(!mustMatch&&0===len)return 0;for(var loweredCase=!1;;)switch(encoding){case"ascii":case"latin1":case"binary":return len;case"utf8":case"utf-8":return utf8ToBytes(string).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*len;case"hex":return len>>>1;case"base64":return base64ToBytes(string).length;default:if(loweredCase)return mustMatch?-1:utf8ToBytes(string).length;encoding=(""+encoding).toLowerCase(),loweredCase=!0}}function slowToString(encoding,start,end){var loweredCase=!1;if((void 0===start||start<0)&&(start=0),start>this.length)return"";if((void 0===end||end>this.length)&&(end=this.length),end<=0)return"";if((end>>>=0)<=(start>>>=0))return"";for(encoding||(encoding="utf8");;)switch(encoding){case"hex":return hexSlice(this,start,end);case"utf8":case"utf-8":return utf8Slice(this,start,end);case"ascii":return asciiSlice(this,start,end);case"latin1":case"binary":return latin1Slice(this,start,end);case"base64":return base64Slice(this,start,end);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,start,end);default:if(loweredCase)throw new TypeError("Unknown encoding: "+encoding);encoding=(encoding+"").toLowerCase(),loweredCase=!0}}function swap(b,n,m){var i=b[n];b[n]=b[m],b[m]=i}function bidirectionalIndexOf(buffer,val,byteOffset,encoding,dir){if(0===buffer.length)return-1;if("string"==typeof byteOffset?(encoding=byteOffset,byteOffset=0):byteOffset>2147483647?byteOffset=2147483647:byteOffset<-2147483648&&(byteOffset=-2147483648),numberIsNaN(byteOffset=+byteOffset)&&(byteOffset=dir?0:buffer.length-1),byteOffset<0&&(byteOffset=buffer.length+byteOffset),byteOffset>=buffer.length){if(dir)return-1;byteOffset=buffer.length-1}else if(byteOffset<0){if(!dir)return-1;byteOffset=0}if("string"==typeof val&&(val=Buffer.from(val,encoding)),Buffer.isBuffer(val))return 0===val.length?-1:arrayIndexOf(buffer,val,byteOffset,encoding,dir);if("number"==typeof val)return val&=255,"function"==typeof Uint8Array.prototype.indexOf?dir?Uint8Array.prototype.indexOf.call(buffer,val,byteOffset):Uint8Array.prototype.lastIndexOf.call(buffer,val,byteOffset):arrayIndexOf(buffer,[val],byteOffset,encoding,dir);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(arr,val,byteOffset,encoding,dir){var i,indexSize=1,arrLength=arr.length,valLength=val.length;if(void 0!==encoding&&("ucs2"===(encoding=String(encoding).toLowerCase())||"ucs-2"===encoding||"utf16le"===encoding||"utf-16le"===encoding)){if(arr.length<2||val.length<2)return-1;indexSize=2,arrLength/=2,valLength/=2,byteOffset/=2}function read(buf,i){return 1===indexSize?buf[i]:buf.readUInt16BE(i*indexSize)}if(dir){var foundIndex=-1;for(i=byteOffset;iarrLength&&(byteOffset=arrLength-valLength),i=byteOffset;i>=0;i--){for(var found=!0,j=0;jremaining&&(length=remaining):length=remaining;var strLen=string.length;length>strLen/2&&(length=strLen/2);for(var i=0;i>8,lo=c%256,byteArray.push(lo),byteArray.push(hi);return byteArray}(string,buf.length-offset),buf,offset,length)}function base64Slice(buf,start,end){return 0===start&&end===buf.length?base64.fromByteArray(buf):base64.fromByteArray(buf.slice(start,end))}function utf8Slice(buf,start,end){end=Math.min(buf.length,end);for(var res=[],i=start;i239?4:firstByte>223?3:firstByte>191?2:1;if(i+bytesPerSequence<=end)switch(bytesPerSequence){case 1:firstByte<128&&(codePoint=firstByte);break;case 2:128==(192&(secondByte=buf[i+1]))&&(tempCodePoint=(31&firstByte)<<6|63&secondByte)>127&&(codePoint=tempCodePoint);break;case 3:secondByte=buf[i+1],thirdByte=buf[i+2],128==(192&secondByte)&&128==(192&thirdByte)&&(tempCodePoint=(15&firstByte)<<12|(63&secondByte)<<6|63&thirdByte)>2047&&(tempCodePoint<55296||tempCodePoint>57343)&&(codePoint=tempCodePoint);break;case 4:secondByte=buf[i+1],thirdByte=buf[i+2],fourthByte=buf[i+3],128==(192&secondByte)&&128==(192&thirdByte)&&128==(192&fourthByte)&&(tempCodePoint=(15&firstByte)<<18|(63&secondByte)<<12|(63&thirdByte)<<6|63&fourthByte)>65535&&tempCodePoint<1114112&&(codePoint=tempCodePoint)}null===codePoint?(codePoint=65533,bytesPerSequence=1):codePoint>65535&&(codePoint-=65536,res.push(codePoint>>>10&1023|55296),codePoint=56320|1023&codePoint),res.push(codePoint),i+=bytesPerSequence}return function(codePoints){var len=codePoints.length;if(len<=MAX_ARGUMENTS_LENGTH)return String.fromCharCode.apply(String,codePoints);var res="",i=0;for(;imax&&(str+=" ... "),""},Buffer.prototype.compare=function(target,start,end,thisStart,thisEnd){if(isInstance(target,Uint8Array)&&(target=Buffer.from(target,target.offset,target.byteLength)),!Buffer.isBuffer(target))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof target);if(void 0===start&&(start=0),void 0===end&&(end=target?target.length:0),void 0===thisStart&&(thisStart=0),void 0===thisEnd&&(thisEnd=this.length),start<0||end>target.length||thisStart<0||thisEnd>this.length)throw new RangeError("out of range index");if(thisStart>=thisEnd&&start>=end)return 0;if(thisStart>=thisEnd)return-1;if(start>=end)return 1;if(this===target)return 0;for(var x=(thisEnd>>>=0)-(thisStart>>>=0),y=(end>>>=0)-(start>>>=0),len=Math.min(x,y),thisCopy=this.slice(thisStart,thisEnd),targetCopy=target.slice(start,end),i=0;i>>=0,isFinite(length)?(length>>>=0,void 0===encoding&&(encoding="utf8")):(encoding=length,length=void 0)}var remaining=this.length-offset;if((void 0===length||length>remaining)&&(length=remaining),string.length>0&&(length<0||offset<0)||offset>this.length)throw new RangeError("Attempt to write outside buffer bounds");encoding||(encoding="utf8");for(var loweredCase=!1;;)switch(encoding){case"hex":return hexWrite(this,string,offset,length);case"utf8":case"utf-8":return utf8Write(this,string,offset,length);case"ascii":return asciiWrite(this,string,offset,length);case"latin1":case"binary":return latin1Write(this,string,offset,length);case"base64":return base64Write(this,string,offset,length);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,string,offset,length);default:if(loweredCase)throw new TypeError("Unknown encoding: "+encoding);encoding=(""+encoding).toLowerCase(),loweredCase=!0}},Buffer.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var MAX_ARGUMENTS_LENGTH=4096;function asciiSlice(buf,start,end){var ret="";end=Math.min(buf.length,end);for(var i=start;ilen)&&(end=len);for(var out="",i=start;ilength)throw new RangeError("Trying to access beyond buffer length")}function checkInt(buf,value,offset,ext,max,min){if(!Buffer.isBuffer(buf))throw new TypeError('"buffer" argument must be a Buffer instance');if(value>max||valuebuf.length)throw new RangeError("Index out of range")}function checkIEEE754(buf,value,offset,ext,max,min){if(offset+ext>buf.length)throw new RangeError("Index out of range");if(offset<0)throw new RangeError("Index out of range")}function writeFloat(buf,value,offset,littleEndian,noAssert){return value=+value,offset>>>=0,noAssert||checkIEEE754(buf,0,offset,4),ieee754.write(buf,value,offset,littleEndian,23,4),offset+4}function writeDouble(buf,value,offset,littleEndian,noAssert){return value=+value,offset>>>=0,noAssert||checkIEEE754(buf,0,offset,8),ieee754.write(buf,value,offset,littleEndian,52,8),offset+8}Buffer.prototype.slice=function(start,end){var len=this.length;(start=~~start)<0?(start+=len)<0&&(start=0):start>len&&(start=len),(end=void 0===end?len:~~end)<0?(end+=len)<0&&(end=0):end>len&&(end=len),end>>=0,byteLength>>>=0,noAssert||checkOffset(offset,byteLength,this.length);for(var val=this[offset],mul=1,i=0;++i>>=0,byteLength>>>=0,noAssert||checkOffset(offset,byteLength,this.length);for(var val=this[offset+--byteLength],mul=1;byteLength>0&&(mul*=256);)val+=this[offset+--byteLength]*mul;return val},Buffer.prototype.readUInt8=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,1,this.length),this[offset]},Buffer.prototype.readUInt16LE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,2,this.length),this[offset]|this[offset+1]<<8},Buffer.prototype.readUInt16BE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,2,this.length),this[offset]<<8|this[offset+1]},Buffer.prototype.readUInt32LE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),(this[offset]|this[offset+1]<<8|this[offset+2]<<16)+16777216*this[offset+3]},Buffer.prototype.readUInt32BE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),16777216*this[offset]+(this[offset+1]<<16|this[offset+2]<<8|this[offset+3])},Buffer.prototype.readIntLE=function(offset,byteLength,noAssert){offset>>>=0,byteLength>>>=0,noAssert||checkOffset(offset,byteLength,this.length);for(var val=this[offset],mul=1,i=0;++i=(mul*=128)&&(val-=Math.pow(2,8*byteLength)),val},Buffer.prototype.readIntBE=function(offset,byteLength,noAssert){offset>>>=0,byteLength>>>=0,noAssert||checkOffset(offset,byteLength,this.length);for(var i=byteLength,mul=1,val=this[offset+--i];i>0&&(mul*=256);)val+=this[offset+--i]*mul;return val>=(mul*=128)&&(val-=Math.pow(2,8*byteLength)),val},Buffer.prototype.readInt8=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,1,this.length),128&this[offset]?-1*(255-this[offset]+1):this[offset]},Buffer.prototype.readInt16LE=function(offset,noAssert){offset>>>=0,noAssert||checkOffset(offset,2,this.length);var val=this[offset]|this[offset+1]<<8;return 32768&val?4294901760|val:val},Buffer.prototype.readInt16BE=function(offset,noAssert){offset>>>=0,noAssert||checkOffset(offset,2,this.length);var val=this[offset+1]|this[offset]<<8;return 32768&val?4294901760|val:val},Buffer.prototype.readInt32LE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),this[offset]|this[offset+1]<<8|this[offset+2]<<16|this[offset+3]<<24},Buffer.prototype.readInt32BE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),this[offset]<<24|this[offset+1]<<16|this[offset+2]<<8|this[offset+3]},Buffer.prototype.readFloatLE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),ieee754.read(this,offset,!0,23,4)},Buffer.prototype.readFloatBE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,4,this.length),ieee754.read(this,offset,!1,23,4)},Buffer.prototype.readDoubleLE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,8,this.length),ieee754.read(this,offset,!0,52,8)},Buffer.prototype.readDoubleBE=function(offset,noAssert){return offset>>>=0,noAssert||checkOffset(offset,8,this.length),ieee754.read(this,offset,!1,52,8)},Buffer.prototype.writeUIntLE=function(value,offset,byteLength,noAssert){(value=+value,offset>>>=0,byteLength>>>=0,noAssert)||checkInt(this,value,offset,byteLength,Math.pow(2,8*byteLength)-1,0);var mul=1,i=0;for(this[offset]=255&value;++i>>=0,byteLength>>>=0,noAssert)||checkInt(this,value,offset,byteLength,Math.pow(2,8*byteLength)-1,0);var i=byteLength-1,mul=1;for(this[offset+i]=255&value;--i>=0&&(mul*=256);)this[offset+i]=value/mul&255;return offset+byteLength},Buffer.prototype.writeUInt8=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,1,255,0),this[offset]=255&value,offset+1},Buffer.prototype.writeUInt16LE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,2,65535,0),this[offset]=255&value,this[offset+1]=value>>>8,offset+2},Buffer.prototype.writeUInt16BE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,2,65535,0),this[offset]=value>>>8,this[offset+1]=255&value,offset+2},Buffer.prototype.writeUInt32LE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,4,4294967295,0),this[offset+3]=value>>>24,this[offset+2]=value>>>16,this[offset+1]=value>>>8,this[offset]=255&value,offset+4},Buffer.prototype.writeUInt32BE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,4,4294967295,0),this[offset]=value>>>24,this[offset+1]=value>>>16,this[offset+2]=value>>>8,this[offset+3]=255&value,offset+4},Buffer.prototype.writeIntLE=function(value,offset,byteLength,noAssert){if(value=+value,offset>>>=0,!noAssert){var limit=Math.pow(2,8*byteLength-1);checkInt(this,value,offset,byteLength,limit-1,-limit)}var i=0,mul=1,sub=0;for(this[offset]=255&value;++i>0)-sub&255;return offset+byteLength},Buffer.prototype.writeIntBE=function(value,offset,byteLength,noAssert){if(value=+value,offset>>>=0,!noAssert){var limit=Math.pow(2,8*byteLength-1);checkInt(this,value,offset,byteLength,limit-1,-limit)}var i=byteLength-1,mul=1,sub=0;for(this[offset+i]=255&value;--i>=0&&(mul*=256);)value<0&&0===sub&&0!==this[offset+i+1]&&(sub=1),this[offset+i]=(value/mul>>0)-sub&255;return offset+byteLength},Buffer.prototype.writeInt8=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,1,127,-128),value<0&&(value=255+value+1),this[offset]=255&value,offset+1},Buffer.prototype.writeInt16LE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,2,32767,-32768),this[offset]=255&value,this[offset+1]=value>>>8,offset+2},Buffer.prototype.writeInt16BE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,2,32767,-32768),this[offset]=value>>>8,this[offset+1]=255&value,offset+2},Buffer.prototype.writeInt32LE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,4,2147483647,-2147483648),this[offset]=255&value,this[offset+1]=value>>>8,this[offset+2]=value>>>16,this[offset+3]=value>>>24,offset+4},Buffer.prototype.writeInt32BE=function(value,offset,noAssert){return value=+value,offset>>>=0,noAssert||checkInt(this,value,offset,4,2147483647,-2147483648),value<0&&(value=4294967295+value+1),this[offset]=value>>>24,this[offset+1]=value>>>16,this[offset+2]=value>>>8,this[offset+3]=255&value,offset+4},Buffer.prototype.writeFloatLE=function(value,offset,noAssert){return writeFloat(this,value,offset,!0,noAssert)},Buffer.prototype.writeFloatBE=function(value,offset,noAssert){return writeFloat(this,value,offset,!1,noAssert)},Buffer.prototype.writeDoubleLE=function(value,offset,noAssert){return writeDouble(this,value,offset,!0,noAssert)},Buffer.prototype.writeDoubleBE=function(value,offset,noAssert){return writeDouble(this,value,offset,!1,noAssert)},Buffer.prototype.copy=function(target,targetStart,start,end){if(!Buffer.isBuffer(target))throw new TypeError("argument should be a Buffer");if(start||(start=0),end||0===end||(end=this.length),targetStart>=target.length&&(targetStart=target.length),targetStart||(targetStart=0),end>0&&end=this.length)throw new RangeError("Index out of range");if(end<0)throw new RangeError("sourceEnd out of bounds");end>this.length&&(end=this.length),target.length-targetStart=0;--i)target[i+targetStart]=this[i+start];else Uint8Array.prototype.set.call(target,this.subarray(start,end),targetStart);return len},Buffer.prototype.fill=function(val,start,end,encoding){if("string"==typeof val){if("string"==typeof start?(encoding=start,start=0,end=this.length):"string"==typeof end&&(encoding=end,end=this.length),void 0!==encoding&&"string"!=typeof encoding)throw new TypeError("encoding must be a string");if("string"==typeof encoding&&!Buffer.isEncoding(encoding))throw new TypeError("Unknown encoding: "+encoding);if(1===val.length){var code=val.charCodeAt(0);("utf8"===encoding&&code<128||"latin1"===encoding)&&(val=code)}}else"number"==typeof val&&(val&=255);if(start<0||this.length>>=0,end=void 0===end?this.length:end>>>0,val||(val=0),"number"==typeof val)for(i=start;i55295&&codePoint<57344){if(!leadSurrogate){if(codePoint>56319){(units-=3)>-1&&bytes.push(239,191,189);continue}if(i+1===length){(units-=3)>-1&&bytes.push(239,191,189);continue}leadSurrogate=codePoint;continue}if(codePoint<56320){(units-=3)>-1&&bytes.push(239,191,189),leadSurrogate=codePoint;continue}codePoint=65536+(leadSurrogate-55296<<10|codePoint-56320)}else leadSurrogate&&(units-=3)>-1&&bytes.push(239,191,189);if(leadSurrogate=null,codePoint<128){if((units-=1)<0)break;bytes.push(codePoint)}else if(codePoint<2048){if((units-=2)<0)break;bytes.push(codePoint>>6|192,63&codePoint|128)}else if(codePoint<65536){if((units-=3)<0)break;bytes.push(codePoint>>12|224,codePoint>>6&63|128,63&codePoint|128)}else{if(!(codePoint<1114112))throw new Error("Invalid code point");if((units-=4)<0)break;bytes.push(codePoint>>18|240,codePoint>>12&63|128,codePoint>>6&63|128,63&codePoint|128)}}return bytes}function base64ToBytes(str){return base64.toByteArray(function(str){if((str=(str=str.split("=")[0]).trim().replace(INVALID_BASE64_RE,"")).length<2)return"";for(;str.length%4!=0;)str+="=";return str}(str))}function blitBuffer(src,dst,offset,length){for(var i=0;i=dst.length||i>=src.length);++i)dst[i+offset]=src[i];return i}function isInstance(obj,type){return obj instanceof type||null!=obj&&null!=obj.constructor&&null!=obj.constructor.name&&obj.constructor.name===type.name}function numberIsNaN(obj){return obj!=obj}}).call(this)}).call(this,require("buffer").Buffer)},{"base64-js":5,buffer:6,ieee754:7}],7:[function(require,module,exports){ 9 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 10 | exports.read=function(buffer,offset,isLE,mLen,nBytes){var e,m,eLen=8*nBytes-mLen-1,eMax=(1<>1,nBits=-7,i=isLE?nBytes-1:0,d=isLE?-1:1,s=buffer[offset+i];for(i+=d,e=s&(1<<-nBits)-1,s>>=-nBits,nBits+=eLen;nBits>0;e=256*e+buffer[offset+i],i+=d,nBits-=8);for(m=e&(1<<-nBits)-1,e>>=-nBits,nBits+=mLen;nBits>0;m=256*m+buffer[offset+i],i+=d,nBits-=8);if(0===e)e=1-eBias;else{if(e===eMax)return m?NaN:1/0*(s?-1:1);m+=Math.pow(2,mLen),e-=eBias}return(s?-1:1)*m*Math.pow(2,e-mLen)},exports.write=function(buffer,value,offset,isLE,mLen,nBytes){var e,m,c,eLen=8*nBytes-mLen-1,eMax=(1<>1,rt=23===mLen?Math.pow(2,-24)-Math.pow(2,-77):0,i=isLE?0:nBytes-1,d=isLE?1:-1,s=value<0||0===value&&1/value<0?1:0;for(value=Math.abs(value),isNaN(value)||value===1/0?(m=isNaN(value)?1:0,e=eMax):(e=Math.floor(Math.log(value)/Math.LN2),value*(c=Math.pow(2,-e))<1&&(e--,c*=2),(value+=e+eBias>=1?rt/c:rt*Math.pow(2,1-eBias))*c>=2&&(e++,c/=2),e+eBias>=eMax?(m=0,e=eMax):e+eBias>=1?(m=(value*c-1)*Math.pow(2,mLen),e+=eBias):(m=value*Math.pow(2,eBias-1)*Math.pow(2,mLen),e=0));mLen>=8;buffer[offset+i]=255&m,i+=d,m/=256,mLen-=8);for(e=e<0;buffer[offset+i]=255&e,i+=d,e/=256,eLen-=8);buffer[offset+i-d]|=128*s}},{}]},{},[1]); 11 | -------------------------------------------------------------------------------- /library-files/snapchat-cta.js: -------------------------------------------------------------------------------- 1 | window.snapchatCta = function() { 2 | console.log('Snapchat CTA clicked'); 3 | if (window.mraid) { 4 | mraid.open('{{ .ClickTrackingUrl }}'); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /one-page.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const base64js = require('base64-js'); 4 | const { minify } = require('terser'); 5 | const btoa = require('btoa'); 6 | const replaceString = require('replace-string'); 7 | const lz4 = require('lz4js'); 8 | 9 | const shared = require('./shared'); 10 | 11 | const config = shared.readConfig(); 12 | 13 | var externFiles = [ 14 | ]; 15 | 16 | function inlineAssets(projectPath) { 17 | return new Promise((resolve, reject) => { 18 | (async function() { 19 | var indexLocation = path.resolve(projectPath, "index.html"); 20 | var indexContents = fs.readFileSync(indexLocation, 'utf-8'); 21 | 22 | var addPatchFile = function (filename) { 23 | var patchLocation = path.resolve(projectPath, filename); 24 | fs.copyFileSync('engine-patches/' + filename, patchLocation); 25 | indexContents = indexContents.replace( 26 | '', 27 | '\n ' 28 | ); 29 | }; 30 | 31 | var addLibraryFile = function (filename) { 32 | var patchLocation = path.resolve(projectPath, filename); 33 | fs.copyFileSync('library-files/' + filename, patchLocation); 34 | indexContents = indexContents.replace( 35 | '', 36 | '\n ' 37 | ); 38 | }; 39 | 40 | (function () { 41 | // Patch the http get function to check for an object being passed to it and return immediately 42 | // so we don't need to Base64 the config.json and take another ~30% hit on file size 43 | // This patch should be done before XHR patch because this wraps around the same function that XHR patch patches 44 | // Patching is done last in, first out so it gets added to the top of the script order in the HTML 45 | console.log("↪️ Adding app http get engine patch"); 46 | addPatchFile('one-page-http-get.js'); 47 | 48 | // XHR request patch. We may need to not use XHR due to restrictions on the hosting service 49 | // such as Facebook playable ads. If that's the case, we will add a patch to override http.get 50 | // and decode the base64 URL ourselves 51 | // Copy the patch to the project directory and add the script to index.html 52 | if (config.one_page.patch_xhr_out) { 53 | console.log("↪️ Adding no XHR engine patch"); 54 | addPatchFile('one-page-no-xhr-request.js'); 55 | } 56 | 57 | // Inline game scripts patch. Some platforms block base64 JS code so this overrides the addition 58 | // of game scripts to the document 59 | if (config.one_page.inline_game_scripts) { 60 | console.log("↪️ Adding inline game script engine patch"); 61 | addPatchFile('one-page-inline-game-scripts.js'); 62 | } 63 | 64 | // MRAID support needs to include the mraid.js file and also force the app to use filltype NONE 65 | // so that it fits in the canvas that is sized by the MRAID implementation on the app. This requires 66 | // patching the CSS too to ensure it is placed correctly in the Window 67 | if (config.one_page.mraid_support) { 68 | console.log("↪️ Adding mraid.js as a library"); 69 | indexContents = indexContents.replace( 70 | '', 71 | '\n ' 72 | ); 73 | 74 | console.log("↪️ Force fill type to be NONE in config.js"); 75 | var configLocation = path.resolve(projectPath, "config.json"); 76 | var configContents = fs.readFileSync(configLocation, 'utf-8'); 77 | var configJson = JSON.parse(configContents); 78 | configJson.application_properties.fillMode = "NONE"; 79 | fs.writeFileSync(configLocation, JSON.stringify(configJson)); 80 | 81 | console.log("↪️ Patch CSS to fill the canvas to the body"); 82 | var cssLocation = path.resolve(projectPath, "styles.css"); 83 | var cssContents = fs.readFileSync(cssLocation, 'utf-8'); 84 | var cssRegex = /#application-canvas\.fill-mode-NONE[\s\S]*?}/; 85 | cssContents = cssContents.replace(cssRegex, '#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }'); 86 | fs.writeFileSync(cssLocation, cssContents); 87 | 88 | console.log("↪️ Adding mraid getMaxSize() call in pc.Application#resizeCanvas"); 89 | addPatchFile('one-page-mraid-resize-canvas.js'); 90 | } 91 | })(); 92 | 93 | // 1. Remove manifest.json and the reference in the index.html 94 | (function() { 95 | console.log("↪️ Removing manifest.json"); 96 | var regex = / *\n/; 97 | indexContents = indexContents.replace(regex, ''); 98 | })(); 99 | 100 | // 2. Remove __modules__.js and the reference in the index.html assuming we aren’t using modules for playable ads. 101 | (function() { 102 | console.log("↪️ Removing __modules__.js"); 103 | 104 | var location = path.resolve(projectPath, "__start__.js"); 105 | var contents = fs.readFileSync(location, 'utf-8'); 106 | 107 | var regex = /if \(window.PRELOAD_MODULES.length\) \{\n\s+loadModules\(window.PRELOAD_MODULES, window.ASSET_PREFIX, \(\) \=\> \{\n\s+configure\(\(\) \=\> \{\n\s+console.timeEnd\('start'\)\;\n\s+\}\)\;\n\s+\}\)\n\s+\} else \{\n\s+configure\(\)\;\n\s+\}/s; 108 | 109 | if (config.one_page.mraid_support) { 110 | // // Adds the following code but minified 111 | // if (window.mraid) { 112 | // if (mraid.getState() !== 'ready') { 113 | // mraid.addEventListener('ready', configure); 114 | // } else { 115 | // configure(); 116 | // } 117 | // } else { 118 | // configure(); 119 | // } 120 | contents = contents.replace(regex, 'window.mraid&&"ready"!==mraid.getState()?mraid.addEventListener("ready",configure):configure();'); 121 | } else { 122 | contents = contents.replace(regex, 'configure();'); 123 | } 124 | 125 | // // Adds the following code for better compatibility with ad networks on accessing the CSS styles 126 | // // in configureCss() 127 | // cssElement = document.createElement('style'); 128 | // cssElement.innerHTML =css; 129 | // document.head.appendChild(cssElement); 130 | 131 | regex = /document\.head\.querySelector\('style'\)\.innerHTML \+= css;/; 132 | contents = contents.replace(regex, 'cssElement=document.createElement("style"),cssElement.innerHTML=css,document.head.appendChild(cssElement);'); 133 | 134 | fs.writeFileSync(location, contents); 135 | })(); 136 | 137 | 138 | // 3. Inline the styles.css contents into index.html in style header. 139 | (function() { 140 | console.log("↪️ Inlining style.css into index.html"); 141 | 142 | var location = path.resolve(projectPath, "styles.css"); 143 | var contents = fs.readFileSync(location, 'utf-8'); 144 | 145 | indexContents = indexContents.replace('', ''); 146 | 147 | var styleRegex = / */; 148 | indexContents = indexContents.replace( 149 | styleRegex, 150 | ''); 151 | })(); 152 | 153 | // 4. Open config.json and replace urls with base64 strings of the files with the correct mime type 154 | // 5. In config.json, remove hashes of all files that have an external URL 155 | await (async function() { 156 | console.log("↪️ Base64 encode all urls in config.json"); 157 | 158 | var location = path.resolve(projectPath, "config.json"); 159 | var contents = fs.readFileSync(location, 'utf-8'); 160 | 161 | // Get the assets and Base64 all the files 162 | 163 | var configJson = JSON.parse(contents); 164 | var assets = configJson.assets; 165 | 166 | var sizeReport = []; 167 | 168 | for (const [key, asset] of Object.entries(assets)) { 169 | if (!Object.prototype.hasOwnProperty.call(assets, key)) { 170 | continue; 171 | } 172 | 173 | // If it's not a file, we can ignore 174 | if (!asset.file) { 175 | continue; 176 | } 177 | 178 | var url = unescape(asset.file.url); 179 | var urlSplit = url.split('.'); 180 | var extension = urlSplit[urlSplit.length - 1]; 181 | 182 | var filepath = path.resolve(projectPath, url); 183 | if (!fs.existsSync(filepath)) { 184 | console.log(" Cannot find file " + filepath + " If it's a loading screen script, please ignore"); 185 | continue; 186 | } 187 | 188 | var fileContents; 189 | var isText = false; 190 | 191 | if (extension === 'js') { 192 | isText = true; 193 | } 194 | 195 | if (isText) { 196 | // Needed as we want to minify the JS code 197 | fileContents = fs.readFileSync(filepath, 'utf-8'); 198 | } else { 199 | fileContents = fs.readFileSync(filepath); 200 | } 201 | 202 | if (urlSplit.length === 0) { 203 | reject('Filename does not have an extension: ' + url); 204 | } 205 | 206 | var mimeprefix = "data:application/octet-stream"; 207 | switch(extension) { 208 | case "png": 209 | mimeprefix = "data:image/png"; 210 | break; 211 | 212 | case "jpeg": 213 | case "jpg": 214 | mimeprefix = "data:image/jpeg"; 215 | break; 216 | 217 | case "json": 218 | // The model and animation loader assumes that the base64 URL will be loaded as a binary 219 | if ((asset.type !== 'model' && asset.type !== 'animation')) { 220 | mimeprefix = "data:application/json"; 221 | } 222 | break; 223 | 224 | case "css": 225 | case "html": 226 | case "txt": 227 | case "glsl": 228 | mimeprefix = "data:text/plain"; 229 | break; 230 | 231 | case "mp4": 232 | mimeprefix = "data:video/mp4"; 233 | break; 234 | 235 | case "js": 236 | // Check loading type as it may be added to the index.html (before/after engine) directly 237 | if (asset.data.loadingType === 0) { 238 | mimeprefix = "data:text/javascript"; 239 | // If it is already minified then don't try to minify it again 240 | if (!url.endsWith('.min.js')) { 241 | fileContents = (await minify(fileContents, { 242 | keep_fnames: true, 243 | ecma: '5' 244 | })).code; 245 | } 246 | } else { 247 | fileContents = ''; 248 | } 249 | break; 250 | } 251 | 252 | var b64; 253 | 254 | if (isText) { 255 | b64 = btoa(unescape(encodeURIComponent(fileContents))); 256 | } else { 257 | var ba = Uint8Array.from(fileContents); 258 | b64 = base64js.fromByteArray(ba); 259 | } 260 | 261 | sizeReport.push({ 262 | name: asset.name, 263 | size: b64.length 264 | }); 265 | 266 | // As we are using an escaped URL, we will search using the original URL 267 | asset.file.url = mimeprefix + ';base64,' + b64; 268 | 269 | // Remove the hash to prevent appending to the URL 270 | asset.file.hash = ""; 271 | }; 272 | 273 | if (process.argv.includes('--size-report')) { 274 | sizeReport.sort((a, b) => b.size - a.size); 275 | console.log("↪️ Size Report"); 276 | sizeReport.forEach((item) => { 277 | console.log(" " + item.name + " - " + item.size + " bytes"); 278 | }); 279 | 280 | const totalSize = sizeReport.reduce((acc, item) => acc + item.size, 0); 281 | console.log(" Total size: " + totalSize + " bytes"); 282 | console.log(" Total asset size in MB: " + (totalSize / 1024 / 1024).toFixed(2) + " MB"); 283 | } 284 | 285 | fs.writeFileSync(location, JSON.stringify(configJson)); 286 | })(); 287 | 288 | // 6. Remove __loading__.js. 289 | (function() { 290 | console.log("↪️ Remove __loading__.js"); 291 | var regex = / *'); 426 | }; 427 | })(); 428 | 429 | fs.writeFileSync(indexLocation, indexContents); 430 | resolve(projectPath); 431 | })(); 432 | }); 433 | } 434 | 435 | async function packageFiles (projectPath) { 436 | return new Promise((resolve, reject) => { 437 | (async function () { 438 | console.log('✔️ Packaging files'); 439 | var indexLocation = path.resolve(projectPath, "index.html"); 440 | 441 | var externFilesConfig = config.one_page.extern_files; 442 | 443 | if (externFilesConfig.enabled) { 444 | // Make a package folder with an assets folder 445 | var packagePath = path.resolve(projectPath, 'package'); 446 | var assetsPath = path.resolve(packagePath, externFilesConfig.folder_name); 447 | 448 | // Create the all the folders using the assets path and recursive creation 449 | fs.mkdirSync(assetsPath, {recursive: true}); 450 | 451 | // Copy files to a new dir 452 | for (const filename of externFiles) { 453 | // As the file could also be in sub directories, we need to recursively create 454 | // folders in the file name path 455 | var filenameAssetsPath = path.resolve(assetsPath, filename); 456 | var targetAssetsPath = path.dirname(filenameAssetsPath); 457 | fs.mkdirSync(targetAssetsPath, {recursive: true}); 458 | fs.copyFileSync(path.resolve(projectPath, filename), filenameAssetsPath); 459 | } 460 | 461 | // Make the changes to file paths in index.html as they can be in a folder 462 | // or need a URL prefix for CDN purposes 463 | var assetFilePrefix = externFilesConfig.external_url_prefix.length > 0 ? externFilesConfig.external_url_prefix + '/' : ''; 464 | assetFilePrefix += externFilesConfig.folder_name.length > 0 ? externFilesConfig.folder_name + '/' : ''; 465 | 466 | var indexContents = fs.readFileSync(indexLocation, 'utf-8'); 467 | 468 | for (const filename of externFiles) { 469 | indexContents = indexContents.replace( 470 | '', 471 | '' 472 | ); 473 | } 474 | fs.writeFileSync(indexLocation, indexContents); 475 | fs.copyFileSync(indexLocation, path.resolve(packagePath, 'index.html')); 476 | 477 | // Zip the package folder contents 478 | var zipOutputPath = path.resolve(__dirname, 'temp/out/' + config.playcanvas.name + '.zip'); 479 | await shared.zipProject(packagePath, zipOutputPath); 480 | 481 | resolve(zipOutputPath); 482 | } else { 483 | var indexOutputPath = path.resolve(__dirname, 'temp/out/' + config.playcanvas.name + '.html'); 484 | if (!fs.existsSync(path.dirname(indexOutputPath))) { 485 | fs.mkdirSync(path.dirname(indexOutputPath), { 486 | recursive: true 487 | }); 488 | } 489 | 490 | fs.copyFileSync(indexLocation, indexOutputPath); 491 | 492 | resolve(indexOutputPath); 493 | } 494 | })() 495 | }); 496 | } 497 | 498 | // Force not to concatenate scripts as they need to be inlined 499 | config.playcanvas.scripts_concatenate = false; 500 | 501 | shared.downloadProject(config, "temp/downloads") 502 | .then((zipLocation) => shared.unzipProject(zipLocation, 'contents')) 503 | .then(inlineAssets) 504 | .then(packageFiles) 505 | .then(outputHtml => console.log("Success", outputHtml)) 506 | .catch(err => console.log("Error", err)); 507 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-api-tools", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "rest-api-tools", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "adm-zip": "^0.4.14", 13 | "base64-js": "^1.5.1", 14 | "btoa": "^1.2.1", 15 | "dotenv": "^8.2.0", 16 | "lz4js": "^0.2.0", 17 | "node-fetch": "^2.6.1", 18 | "replace-string": "^3.1.0", 19 | "terser": "^5.5.1" 20 | } 21 | }, 22 | "node_modules/adm-zip": { 23 | "version": "0.4.14", 24 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", 25 | "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==", 26 | "engines": { 27 | "node": ">=0.3.0" 28 | } 29 | }, 30 | "node_modules/base64-js": { 31 | "version": "1.5.1", 32 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 33 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 34 | "funding": [ 35 | { 36 | "type": "github", 37 | "url": "https://github.com/sponsors/feross" 38 | }, 39 | { 40 | "type": "patreon", 41 | "url": "https://www.patreon.com/feross" 42 | }, 43 | { 44 | "type": "consulting", 45 | "url": "https://feross.org/support" 46 | } 47 | ] 48 | }, 49 | "node_modules/btoa": { 50 | "version": "1.2.1", 51 | "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", 52 | "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", 53 | "bin": { 54 | "btoa": "bin/btoa.js" 55 | }, 56 | "engines": { 57 | "node": ">= 0.4.0" 58 | } 59 | }, 60 | "node_modules/buffer-from": { 61 | "version": "1.1.1", 62 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 63 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 64 | }, 65 | "node_modules/commander": { 66 | "version": "2.20.3", 67 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 68 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 69 | }, 70 | "node_modules/dotenv": { 71 | "version": "8.2.0", 72 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 73 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", 74 | "engines": { 75 | "node": ">=8" 76 | } 77 | }, 78 | "node_modules/lz4js": { 79 | "version": "0.2.0", 80 | "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", 81 | "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==" 82 | }, 83 | "node_modules/node-fetch": { 84 | "version": "2.6.1", 85 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 86 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 87 | "engines": { 88 | "node": "4.x || >=6.0.0" 89 | } 90 | }, 91 | "node_modules/replace-string": { 92 | "version": "3.1.0", 93 | "resolved": "https://registry.npmjs.org/replace-string/-/replace-string-3.1.0.tgz", 94 | "integrity": "sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==", 95 | "engines": { 96 | "node": ">=8" 97 | }, 98 | "funding": { 99 | "url": "https://github.com/sponsors/sindresorhus" 100 | } 101 | }, 102 | "node_modules/source-map": { 103 | "version": "0.7.3", 104 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 105 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 106 | "engines": { 107 | "node": ">= 8" 108 | } 109 | }, 110 | "node_modules/source-map-support": { 111 | "version": "0.5.19", 112 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 113 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 114 | "dependencies": { 115 | "buffer-from": "^1.0.0", 116 | "source-map": "^0.6.0" 117 | } 118 | }, 119 | "node_modules/source-map-support/node_modules/source-map": { 120 | "version": "0.6.1", 121 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 122 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 123 | "engines": { 124 | "node": ">=0.10.0" 125 | } 126 | }, 127 | "node_modules/terser": { 128 | "version": "5.5.1", 129 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", 130 | "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", 131 | "dependencies": { 132 | "commander": "^2.20.0", 133 | "source-map": "~0.7.2", 134 | "source-map-support": "~0.5.19" 135 | }, 136 | "bin": { 137 | "terser": "bin/terser" 138 | }, 139 | "engines": { 140 | "node": ">=10" 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-api-tools", 3 | "version": "1.0.0", 4 | "description": "A suite of node scripts to use the PlayCanvas REST API", 5 | "scripts": { 6 | "download": "node download-project.js", 7 | "csp": "node csp-patch.js", 8 | "archive": "node archive-project.js", 9 | "archive-all": "node archive-project-all.js", 10 | "test-one-page": "node tests/test-one-page.js", 11 | "one-page": "node one-page.js", 12 | "cordova-publish": "node cordova-publish.js" 13 | }, 14 | "author": "support@playcanvas.com", 15 | "license": "MIT", 16 | "dependencies": { 17 | "adm-zip": "^0.4.14", 18 | "base64-js": "^1.5.1", 19 | "btoa": "^1.2.1", 20 | "dotenv": "^8.2.0", 21 | "lz4js": "^0.2.0", 22 | "node-fetch": "^2.6.1", 23 | "replace-string": "^3.1.0", 24 | "terser": "^5.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shared.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const dotenv = require('dotenv') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const Zip = require('adm-zip'); 6 | 7 | 8 | function readConfig() { 9 | const env = dotenv.config().parsed; 10 | const configStr = fs.readFileSync('config.json', 'utf-8'); 11 | const config = JSON.parse(configStr); 12 | config.authToken = env['AUTH_TOKEN']; 13 | 14 | // Add defaults if they don't exist 15 | config.csp = config.csp || {}; 16 | config.csp['style-src'] = config.csp['style-src'] || []; 17 | config.csp['connect-src'] = config.csp['connect-src'] || []; 18 | config.csp.patch_preload_bundles = config.csp.patch_preload_bundles || false; 19 | 20 | config.one_page = config.one_page || {}; 21 | config.one_page.patch_xhr_out = config.one_page.patch_xhr_out || false; 22 | config.one_page.inline_game_scripts = config.one_page.inline_game_scripts || false; 23 | config.one_page.mraid_support = config.one_page.mraid_support || false; 24 | config.one_page.snapchat_cta = config.one_page.snapchat_cta || false; 25 | 26 | // Mon 17 May 2021: Backwards compatibility when this used to be a boolean 27 | // and convert to an object 28 | var onePageExternFiles = config.one_page.extern_files; 29 | if (onePageExternFiles) { 30 | if (typeof onePageExternFiles === 'boolean') { 31 | onePageExternFiles = { 32 | enabled: onePageExternFiles 33 | } 34 | } 35 | } 36 | 37 | config.one_page.compress_engine = config.one_page.compress_engine || ''; 38 | 39 | onePageExternFiles = onePageExternFiles || { enabled: false }; 40 | onePageExternFiles.folder_name = onePageExternFiles.folder_name || ''; 41 | onePageExternFiles.external_url_prefix = onePageExternFiles.external_url_prefix || ''; 42 | 43 | config.one_page.extern_files = onePageExternFiles; 44 | 45 | return config; 46 | } 47 | 48 | function pollJob(config, jobId) { 49 | var self = this; 50 | return new Promise((resolve, reject) => { 51 | console.log("↪️ Polling job ", jobId) 52 | fetch('https://playcanvas.com/api/jobs/' + jobId, { 53 | method: 'GET', 54 | headers: { 55 | 'Content-Type': 'application/json', 56 | 'Authorization': 'Bearer ' + config.authToken 57 | } 58 | }) 59 | .then(res => res.json()) 60 | .then((json) => { 61 | if (json.status == "complete") { 62 | console.log("✔️ Job complete!",) 63 | resolve(json.data) 64 | } else if (json.status == "error") { 65 | console.log(" job error ", json.messages) 66 | reject(new Error(json.messages.join(';'))) 67 | } else if (json.status == "running") { 68 | console.log(" job still running"); 69 | return waitAndRetry(config, jobId, resolve); 70 | } 71 | }) 72 | }); 73 | } 74 | 75 | function waitAndRetry(config, jobId, callback) { 76 | return new Promise(resolve => { 77 | console.log(" will wait 1s and then retry") 78 | sleep(1000) 79 | .then(() => pollJob(config, jobId)) 80 | .then(callback); // nested promises anyone? 81 | }) 82 | } 83 | 84 | function sleep(ms) { 85 | return new Promise(resolve => setTimeout(resolve, ms)); 86 | } 87 | 88 | function downloadProject(config, directory) { 89 | return new Promise((resolve, reject) => { 90 | console.log("✔️ Requested build from Playcanvas") 91 | fetch('https://playcanvas.com/api/apps/download', { 92 | method: 'POST', 93 | body: JSON.stringify({ 94 | "project_id": parseInt(config.playcanvas.project_id), 95 | "name": config.playcanvas.name, 96 | "scenes": config.playcanvas.scenes, 97 | "branch_id": config.playcanvas.branch_id, 98 | "description": config.playcanvas.description, 99 | "preload_bundle": config.playcanvas.preload_bundle, 100 | "version": config.playcanvas.version, 101 | "release_notes": config.playcanvas.release_notes, 102 | "scripts_concatenate": config.playcanvas.scripts_concatenate, 103 | "scripts_minify": config.playcanvas.scripts_minify, 104 | "optimize_scene_format": config.playcanvas.optimize_scene_format, 105 | "engine_version": config.playcanvas.engine_version 106 | }), 107 | headers: { 108 | 'Content-Type': 'application/json', 109 | 'Authorization': 'Bearer ' + config.authToken 110 | } 111 | }) 112 | .then(res => { 113 | if (res.status !== 201) { 114 | throw new Error("Error: status code " + res.status); 115 | } 116 | return res.json(); 117 | }) 118 | .then(buildJob => pollJob(config, buildJob.id)) 119 | .then(json => { 120 | console.log("✔ Downloading zip", json.download_url); 121 | return fetch(json.download_url, {method: 'GET'}) 122 | }) 123 | .then(res => res.buffer()) 124 | .then(buffer => { 125 | let output = path.resolve(__dirname, directory + "/" + config.playcanvas.name + '_Download.zip'); 126 | if (!fs.existsSync(path.dirname(output))) { 127 | fs.mkdirSync(path.dirname(output), {recursive:true}); 128 | } 129 | fs.writeFileSync(output, buffer, 'binary') 130 | resolve(output); 131 | }) 132 | .catch(reject); 133 | }); 134 | } 135 | 136 | function archiveProject(config, branchName, branchId, directory) { 137 | return new Promise((resolve, reject) => { 138 | console.log("✔️ Requested archive from Playcanvas") 139 | fetch('https://playcanvas.com/api/projects/' + config.playcanvas.project_id + '/export', { 140 | method: 'POST', 141 | body: JSON.stringify({ 142 | "branch_id": branchId 143 | }), 144 | headers: { 145 | 'Content-Type': 'application/json', 146 | 'Authorization': 'Bearer ' + config.authToken 147 | } 148 | }) 149 | .then(res => { 150 | if (res.status !== 200) { 151 | throw new Error("Error: status code " + res.status); 152 | } 153 | return res.json(); 154 | }) 155 | .then(buildJob => pollJob(config, buildJob.id)) 156 | .then(json => { 157 | console.log("✔ Downloading zip", json.url); 158 | return fetch(json.url, {method: 'GET'}) 159 | }) 160 | .then(res => res.buffer()) 161 | .then(buffer => { 162 | let output = path.resolve(__dirname, directory + "/" + config.playcanvas.name + '_Archive_' + branchName + '.zip'); 163 | if (!fs.existsSync(path.dirname(output))) { 164 | fs.mkdirSync(path.dirname(output), {recursive:true}); 165 | } 166 | fs.writeFileSync(output, buffer, 'binary') 167 | resolve(output); 168 | }) 169 | .catch(reject); 170 | }); 171 | } 172 | 173 | function unzipProject(zipLocation, unzipFolderName) { 174 | return new Promise((resolve, reject) => { 175 | console.log('✔️ Unzipping ', zipLocation); 176 | var zipFile = new Zip(zipLocation); 177 | try { 178 | var tempFolder = path.resolve(path.dirname(zipLocation), unzipFolderName); 179 | if (fs.existsSync(tempFolder)) { 180 | fs.rmSync(tempFolder, {recursive:true}); 181 | } 182 | fs.mkdirSync(tempFolder); 183 | zipFile.extractAllTo(tempFolder, true); 184 | resolve(tempFolder); 185 | } catch (e) { 186 | reject(e); 187 | } 188 | }); 189 | } 190 | 191 | function zipProject(rootFolder, targetLocation) { 192 | return new Promise((resolve, reject) => { 193 | console.log("✔️ Zipping it all back again") 194 | let output = path.resolve(__dirname, targetLocation); 195 | var zip = new Zip(); 196 | zip.addLocalFolder(rootFolder); 197 | if (!fs.existsSync(path.dirname(output))) { 198 | fs.mkdirSync(path.dirname(output)); 199 | } 200 | zip.writeZip(output); 201 | fs.rmSync(rootFolder, {recursive:true}); 202 | resolve(output); 203 | }); 204 | } 205 | 206 | module.exports = { readConfig, sleep, downloadProject, archiveProject, unzipProject, zipProject}; 207 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.cubejump-mraid-interstitial.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 775981, 4 | "name": "Cube Jump MRAID Interstitial", 5 | "scenes" : [1113087], 6 | "branch_name": "master", 7 | "branch_id": "94c72be8-ae43-4142-9d2a-6ea31039be11", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": true, 25 | "extern_files": false, 26 | "mraid_support": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.cubejump-playable-fb.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 354998, 4 | "name": "Cube Jumper Playable FB", 5 | "scenes" : [380870], 6 | "branch_name": "master", 7 | "branch_id": "3c2266b4-4ec7-4616-959e-cf41833e99cf", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": true, 24 | "inline_game_scripts": true, 25 | "extern_files": false, 26 | "mraid_support": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.cubejump-snapchat-ad.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 796932, 4 | "name": "Cube Jump Snapchat Ad", 5 | "scenes" : [1155440], 6 | "branch_name": "master", 7 | "branch_id": "49d50dc7-2e23-4314-8f5c-bd9ec1b5f7aa", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": true, 25 | "extern_files": { 26 | "enabled": true, 27 | "folder_name": "78fb9255-3033-4fe2-b9e1-355b149229a1", 28 | "external_url_prefix": "" 29 | }, 30 | "mraid_support": true, 31 | "snapchat_cta": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.fileadaudit-mraid-interstitial.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 749556, 4 | "name": "File Ad Audit MRAID Interstitial", 5 | "scenes" : [1059411], 6 | "branch_name": "master", 7 | "branch_id": "26ea4215-c688-46a2-8b62-f61beca75cd9", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": true, 25 | "extern_files": false, 26 | "mraid_support": true 27 | } 28 | } -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.flappy-compressed-engine.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 375389, 4 | "name": "Flappy Bird Compressed Engine", 5 | "scenes" : [404993], 6 | "branch_name": "master", 7 | "branch_id": "605cd6ab-17b4-4dac-9960-ad439567c957", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": false, 25 | "extern_files": false, 26 | "mraid_support": false, 27 | "compress_engine": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.flappy-mraid-interstitial.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 783639, 4 | "name": "Flappy Bird MRAID Interstitial", 5 | "scenes" : [1128241], 6 | "branch_name": "master", 7 | "branch_id": "d43af44b-0f60-40f2-a8cf-abaf544d1e11", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": true, 25 | "extern_files": false, 26 | "mraid_support": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.flappy-playable-fb.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 785157, 4 | "name": "Flappy Bird Playable FB", 5 | "scenes" : [1131060], 6 | "branch_name": "master", 7 | "branch_id": "31ff2bba-9f97-4b4d-933b-e908e06c6429", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": true, 24 | "inline_game_scripts": true, 25 | "extern_files": false, 26 | "mraid_support": false 27 | } 28 | } -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.flappy.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 375389, 4 | "name": "Flappy Bird", 5 | "scenes" : [404993], 6 | "branch_name": "master", 7 | "branch_id": "605cd6ab-17b4-4dac-9960-ad439567c957", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": false, 25 | "extern_files": false, 26 | "mraid_support": false 27 | } 28 | } -------------------------------------------------------------------------------- /tests/configs-one-page/config.json.xwing-extern-files.json: -------------------------------------------------------------------------------- 1 | { 2 | "playcanvas": { 3 | "project_id": 395232, 4 | "name": "X Wing Extern Files", 5 | "scenes" : [427806], 6 | "branch_name": "master", 7 | "branch_id": "dfbe2442-0acf-49c2-8f5f-f32aa7aa6729", 8 | "description": "", 9 | "preload_bundle": false, 10 | "version": "", 11 | "release_notes": "", 12 | "scripts_concatenate": true, 13 | "scripts_sourcemaps": false, 14 | "optimize_scene_format": false 15 | }, 16 | "csp": { 17 | "style-src": [ 18 | ], 19 | "connect-src": [ 20 | ] 21 | }, 22 | "one_page": { 23 | "patch_xhr_out": false, 24 | "inline_game_scripts": false, 25 | "extern_files": { 26 | "enabled": true, 27 | "folder_name": "assets" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/test-one-page.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const childProcess = require('child_process'); 4 | const { performance } = require('perf_hooks'); 5 | 6 | const configsFolder = 'tests/configs-one-page'; 7 | 8 | 9 | function sleep(ms) { 10 | return new Promise((resolve) => { 11 | setTimeout(resolve, ms); 12 | }); 13 | } 14 | 15 | // Go through all the public test configs to create the most common 16 | // outputs for the one-page job 17 | (async function() { 18 | const files = fs.readdirSync(configsFolder); 19 | const maxJobsPerMinute = 5; 20 | const msInAMinute = 60 * 1000; 21 | 22 | let msTakenSoFar = 0; 23 | 24 | for (let i = 0; i < files.length; ++i) { 25 | let timeStart = performance.now(); 26 | let file = files[i]; 27 | 28 | // Skip hidden files 29 | if (file.startsWith('.')) { 30 | continue; 31 | } 32 | 33 | let fromPath = path.join(configsFolder, file); 34 | let toPath = path.join('', 'config.json'); 35 | 36 | console.log('Using config \'' + file + '\''); 37 | 38 | fs.copyFileSync(fromPath, toPath); 39 | let output = childProcess.execSync('npm run one-page', {encoding: 'utf-8'}); 40 | 41 | console.log(output); 42 | 43 | let timeEnd = performance.now(); 44 | msTakenSoFar += (timeEnd - timeStart); 45 | 46 | // If we are not the last file, make sure to add some time between batches of REST API 47 | // calls so that we don't hit the strict rate limit 48 | if (i !== (files.length - 1)) { 49 | if (i % maxJobsPerMinute === (maxJobsPerMinute - 1)) { 50 | if (msTakenSoFar <= msInAMinute) { 51 | let msTillRateLimitEnds = msInAMinute - msTakenSoFar; 52 | console.log('Waiting till rate limit has passed (' + (msTillRateLimitEnds / 1000).toFixed(2) + 's)'); 53 | await sleep(msInAMinute - msTakenSoFar); 54 | } 55 | msTakenSoFar = 0; 56 | } 57 | } 58 | } 59 | })(); 60 | --------------------------------------------------------------------------------