├── .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 |
--------------------------------------------------------------------------------