@playcraft/build 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,7 +148,7 @@ export declare class PlayableScriptsAdapter {
148
148
  private copyToOutputDir;
149
149
  /**
150
150
  * 是否对 playable-scripts 使用 --zip。
151
- * Google/Facebook 等双格式渠道默认 HTML,与 CLI、Portal、GoogleAdapter 一致;仅 ZIP 的渠道仍强制 ZIP
151
+ * 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip
152
152
  */
153
153
  private isZipOutput;
154
154
  /**
@@ -258,7 +258,7 @@ export class PlayableScriptsAdapter {
258
258
  // 输出目录 - 统一使用 dist,与 PlayCraft 标准一致
259
259
  const outputDir = config.outputDir ?? 'dist';
260
260
  args.push('--out-dir', outputDir);
261
- // ZIP 输出:仅 ZIP-only 渠道、显式 config.zip、或构建请求 format=zip(如 Google / Facebook ZIP)
261
+ // ZIP 输出:显式 config.zip 或构建请求 format=zip(如 Google / Facebook / TikTok 等选 ZIP)
262
262
  const isZipFormat = this.isZipOutput();
263
263
  if (isZipFormat) {
264
264
  args.push('--zip');
@@ -1201,25 +1201,13 @@ export class PlayableScriptsAdapter {
1201
1201
  }
1202
1202
  /**
1203
1203
  * 是否对 playable-scripts 使用 --zip。
1204
- * Google/Facebook 等双格式渠道默认 HTML,与 CLI、Portal、GoogleAdapter 一致;仅 ZIP 的渠道仍强制 ZIP
1204
+ * 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip
1205
1205
  */
1206
1206
  isZipOutput() {
1207
1207
  const config = this.config ?? {};
1208
- const channels = config.channels ?? ['google'];
1209
1208
  if (config.zip === true) {
1210
1209
  return true;
1211
1210
  }
1212
- const zipOnlyPlatforms = new Set([
1213
- 'tiktok',
1214
- 'snapchat',
1215
- 'liftoff',
1216
- 'bigo',
1217
- 'inmobi',
1218
- 'mintegral',
1219
- ]);
1220
- if (channels.some((ch) => zipOnlyPlatforms.has(ch))) {
1221
- return true;
1222
- }
1223
1211
  const fmt = this.options.format;
1224
1212
  return fmt === 'zip';
1225
1213
  }
@@ -8,7 +8,7 @@ export class BigoAdapter extends PlatformAdapter {
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // BIGO Ads 需要 BIGO JS-SDK
@@ -36,7 +36,7 @@ export class BigoAdapter extends PlatformAdapter {
36
36
  `;
37
37
  const sdkScript = `<script src="https://static-web.likeevideo.com/oss/material-ad/playable-ad-demo/bgy-mraid-sdk.js"></script>`;
38
38
  const fallbackScript = await this.minifyPlatformScript(bigoScriptCode, 'bigo-sdk');
39
- const comment = `<!-- BIGO: ZIP格式, config.jsonorientation, 使用BGY_MRAID.open() -->`;
39
+ const comment = `<!-- BIGO: HTML/ZIP format, config.json includes orientation when present, use BGY_MRAID.open() -->`;
40
40
  // 在 </head> 之前插入
41
41
  html = html.replace('</head>', `${sdkScript}${fallbackScript}${comment}</head>`);
42
42
  // 注入统一的 CTA 适配器
@@ -71,8 +71,6 @@ export class BigoAdapter extends PlatformAdapter {
71
71
  };
72
72
  }
73
73
  validateOptions() {
74
- if (this.options.format && this.options.format !== 'zip') {
75
- console.warn('警告: BIGO Ads 要求 ZIP 格式');
76
- }
74
+ // BIGO Ads supports both HTML and ZIP output in PlayCraft.
77
75
  }
78
76
  }
@@ -8,7 +8,7 @@ export class InMobiAdapter extends PlatformAdapter {
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // InMobi 需要 MRAID 支持
@@ -36,7 +36,7 @@ export class InMobiAdapter extends PlatformAdapter {
36
36
  };
37
37
  `;
38
38
  const inmobiScript = await this.minifyPlatformScript(inmobiScriptCode, 'inmobi-mraid');
39
- const comment = `<!-- InMobi: ZIP格式, 使用mraid.open() -->`;
39
+ const comment = `<!-- InMobi: HTML/ZIP format, use mraid.open() -->`;
40
40
  // 在 </head> 之前插入平台特定的 API
41
41
  html = html.replace('</head>', `${inmobiScript}${comment}</head>`);
42
42
  // 注入统一的 CTA 适配器
@@ -57,8 +57,6 @@ export class InMobiAdapter extends PlatformAdapter {
57
57
  `;
58
58
  }
59
59
  validateOptions() {
60
- if (this.options.format && this.options.format !== 'zip') {
61
- console.warn('警告: InMobi 要求 ZIP 格式');
62
- }
60
+ // InMobi supports both HTML and ZIP output in PlayCraft.
63
61
  }
64
62
  }
@@ -8,7 +8,7 @@ export class LiftoffAdapter extends PlatformAdapter {
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // Liftoff 支持 MRAID 或 window.open
@@ -31,7 +31,7 @@ export class LiftoffAdapter extends PlatformAdapter {
31
31
  };
32
32
  `;
33
33
  const liftoffScript = await this.minifyPlatformScript(liftoffScriptCode, 'liftoff-mraid');
34
- const comment = `<!-- Liftoff: ZIP格式, ASCII文件名, 使用mraid.open()window.open() -->`;
34
+ const comment = `<!-- Liftoff: HTML/ZIP format, ASCII filenames recommended, use mraid.open() or window.open() -->`;
35
35
  // 在 </head> 之前插入
36
36
  html = html.replace('</head>', `${liftoffScript}${comment}</head>`);
37
37
  // 注入统一的 CTA 适配器
@@ -47,8 +47,6 @@ export class LiftoffAdapter extends PlatformAdapter {
47
47
  `;
48
48
  }
49
49
  validateOptions() {
50
- if (this.options.format && this.options.format !== 'zip') {
51
- console.warn('警告: Liftoff 要求 ZIP 格式');
52
- }
50
+ // Liftoff supports both HTML and ZIP output in PlayCraft.
53
51
  }
54
52
  }
@@ -4,11 +4,11 @@ export class MintegralAdapter extends PlatformAdapter {
4
4
  return 'Mintegral';
5
5
  }
6
6
  getSizeLimit() {
7
- // Mintegral: 5MB (ZIP)
7
+ // Mintegral: 5MB
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // Mintegral 要求 charset 为 utf-8,确保存在
@@ -33,7 +33,7 @@ export class MintegralAdapter extends PlatformAdapter {
33
33
  window.HttpAPI = window.HttpAPI || { sendPoint: function() {} };
34
34
  `;
35
35
  const mintegralScript = await this.minifyPlatformScript(mintegralScriptCode, 'mintegral-sdk');
36
- const comment = `<!-- Mintegral: ZIP格式, 5MB限制, 使用window.install()跳转, 支持横竖屏 -->`;
36
+ const comment = `<!-- Mintegral: HTML/ZIP format, 5MB limit, use window.install(), supports both orientations -->`;
37
37
  // 在 </head> 之前插入 polyfill 和注释,如果没有 </head> 则在文件开头插入
38
38
  if (html.includes('</head>')) {
39
39
  html = html.replace('</head>', `${mintegralScript}${comment}\n</head>`);
@@ -59,8 +59,6 @@ export class MintegralAdapter extends PlatformAdapter {
59
59
  `;
60
60
  }
61
61
  validateOptions() {
62
- if (this.options.format && this.options.format !== 'zip') {
63
- console.warn('警告: Mintegral 要求 ZIP 格式,将自动使用 ZIP');
64
- }
62
+ // Mintegral supports both HTML and ZIP output in PlayCraft.
65
63
  }
66
64
  }
@@ -8,7 +8,7 @@ export class SnapchatAdapter extends PlatformAdapter {
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // Snapchat 需要 MRAID 2.0 和 snapchatCta 函数
@@ -8,7 +8,7 @@ export class TikTokAdapter extends PlatformAdapter {
8
8
  return 5 * 1024 * 1024;
9
9
  }
10
10
  getDefaultFormat() {
11
- return 'zip';
11
+ return 'html';
12
12
  }
13
13
  async modifyHTML(html, assets) {
14
14
  // TikTok/Pangle 需要 Pangle JS-SDK
@@ -35,7 +35,7 @@ export class TikTokAdapter extends PlatformAdapter {
35
35
  };
36
36
  `;
37
37
  const pangleScript = await this.minifyPlatformScript(pangleScriptCode, 'pangle-sdk');
38
- const comment = `<!-- TikTok/Pangle: ZIP格式, config.jsonplayable_orientation, 禁止mraid.js -->`;
38
+ const comment = `<!-- TikTok/Pangle: HTML/ZIP format, config.json includes playable_orientation when present, no mraid.js -->`;
39
39
  // 在 </head> 之前插入
40
40
  html = html.replace('</head>', `${pangleScript}${comment}</head>`);
41
41
  // 注入统一的 CTA 适配器
@@ -63,8 +63,6 @@ export class TikTokAdapter extends PlatformAdapter {
63
63
  };
64
64
  }
65
65
  validateOptions() {
66
- if (this.options.format && this.options.format !== 'zip') {
67
- console.warn('警告: TikTok/Pangle 要求 ZIP 格式');
68
- }
66
+ // TikTok/Pangle supports both HTML and ZIP output in PlayCraft.
69
67
  }
70
68
  }
@@ -67,7 +67,7 @@ export const PLATFORM_CONFIGS = {
67
67
  },
68
68
  snapchat: {
69
69
  sizeLimit: 5 * 1024 * 1024, // 5MB
70
- outputFormat: 'zip',
70
+ outputFormat: 'html',
71
71
  minifyCSS: true,
72
72
  minifyJS: true,
73
73
  compressImages: false, // 禁用图片压缩,原图输出效果更好
@@ -185,7 +185,7 @@ export const PLATFORM_CONFIGS = {
185
185
  },
186
186
  tiktok: {
187
187
  sizeLimit: 5 * 1024 * 1024, // 5MB
188
- outputFormat: 'zip',
188
+ outputFormat: 'html',
189
189
  minifyCSS: true,
190
190
  minifyJS: true,
191
191
  compressImages: false, // 禁用图片压缩,原图输出效果更好
@@ -243,7 +243,7 @@ export const PLATFORM_CONFIGS = {
243
243
  },
244
244
  liftoff: {
245
245
  sizeLimit: 5 * 1024 * 1024, // 5MB
246
- outputFormat: 'zip',
246
+ outputFormat: 'html',
247
247
  minifyCSS: true,
248
248
  minifyJS: true,
249
249
  compressImages: false, // 禁用图片压缩,原图输出效果更好
@@ -301,7 +301,7 @@ export const PLATFORM_CONFIGS = {
301
301
  },
302
302
  bigo: {
303
303
  sizeLimit: 5 * 1024 * 1024, // 5MB
304
- outputFormat: 'zip',
304
+ outputFormat: 'html',
305
305
  minifyCSS: true,
306
306
  minifyJS: true,
307
307
  compressImages: false, // 禁用图片压缩,原图输出效果更好
@@ -329,7 +329,7 @@ export const PLATFORM_CONFIGS = {
329
329
  },
330
330
  inmobi: {
331
331
  sizeLimit: 5 * 1024 * 1024, // 5MB
332
- outputFormat: 'zip',
332
+ outputFormat: 'html',
333
333
  minifyCSS: true,
334
334
  minifyJS: true,
335
335
  compressImages: false, // 禁用图片压缩,原图输出效果更好
@@ -416,8 +416,8 @@ export const PLATFORM_CONFIGS = {
416
416
  },
417
417
  },
418
418
  mintegral: {
419
- sizeLimit: 5 * 1024 * 1024, // 5MB (ZIP)
420
- outputFormat: 'zip',
419
+ sizeLimit: 5 * 1024 * 1024, // 5MB
420
+ outputFormat: 'html',
421
421
  minifyCSS: true,
422
422
  minifyJS: true,
423
423
  compressImages: false, // 禁用图片压缩,原图输出效果更好
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,169 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { OptimizationAnalyzer } from '../optimization-analyzer.js';
3
- describe('OptimizationAnalyzer', () => {
4
- it('should detect large uncompressed files', () => {
5
- const state = {
6
- version: '1.0.0',
7
- buildTime: Date.now(),
8
- assets: {
9
- 'large-file': {
10
- id: 'large-file',
11
- originalName: 'large-file.js',
12
- originalPath: '/path/to/large-file.js',
13
- originalSize: 200 * 1024, // 200KB
14
- finalSize: 195 * 1024, // 只压缩了 5KB
15
- totalCompressionRatio: 0.025, // 2.5% 压缩率
16
- type: 'script',
17
- processingHistory: [
18
- {
19
- stage: 'base-build',
20
- name: 'large-file.js',
21
- size: 195 * 1024,
22
- optimizations: [],
23
- },
24
- ],
25
- },
26
- },
27
- stages: [],
28
- };
29
- const analyzer = new OptimizationAnalyzer(state);
30
- const result = analyzer.analyze();
31
- expect(result.totalSuggestions).toBeGreaterThan(0);
32
- expect(result.suggestions.some(s => s.type === 'large-uncompressed')).toBe(true);
33
- });
34
- it('should detect PNG images that can be converted to WebP', () => {
35
- const state = {
36
- version: '1.0.0',
37
- buildTime: Date.now(),
38
- assets: {
39
- 'image': {
40
- id: 'image',
41
- originalName: 'image.png',
42
- originalPath: '/path/to/image.png',
43
- originalSize: 100 * 1024, // 100KB
44
- finalSize: 100 * 1024,
45
- totalCompressionRatio: 0,
46
- type: 'texture',
47
- processingHistory: [
48
- {
49
- stage: 'base-build',
50
- name: 'image.png',
51
- size: 100 * 1024,
52
- optimizations: [],
53
- },
54
- ],
55
- },
56
- },
57
- stages: [],
58
- };
59
- const analyzer = new OptimizationAnalyzer(state);
60
- const result = analyzer.analyze();
61
- expect(result.suggestions.some(s => s.type === 'format-conversion')).toBe(true);
62
- });
63
- it('should detect missing minification', () => {
64
- const state = {
65
- version: '1.0.0',
66
- buildTime: Date.now(),
67
- assets: {
68
- 'script': {
69
- id: 'script',
70
- originalName: 'script.js',
71
- originalPath: '/path/to/script.js',
72
- originalSize: 50 * 1024, // 50KB
73
- finalSize: 50 * 1024,
74
- totalCompressionRatio: 0,
75
- type: 'script',
76
- processingHistory: [
77
- {
78
- stage: 'base-build',
79
- name: 'script.js',
80
- size: 50 * 1024,
81
- optimizations: [], // 没有 minify
82
- },
83
- ],
84
- },
85
- },
86
- stages: [],
87
- };
88
- const analyzer = new OptimizationAnalyzer(state);
89
- const result = analyzer.analyze();
90
- expect(result.suggestions.some(s => s.type === 'enable-minify')).toBe(true);
91
- });
92
- it('should group suggestions by severity', () => {
93
- const state = {
94
- version: '1.0.0',
95
- buildTime: Date.now(),
96
- assets: {
97
- 'large-uncompressed': {
98
- id: 'large-uncompressed',
99
- originalName: 'large.js',
100
- originalPath: '/path/to/large.js',
101
- originalSize: 200 * 1024,
102
- finalSize: 195 * 1024,
103
- totalCompressionRatio: 0.025,
104
- type: 'script',
105
- processingHistory: [
106
- {
107
- stage: 'base-build',
108
- name: 'large.js',
109
- size: 195 * 1024,
110
- optimizations: [],
111
- },
112
- ],
113
- },
114
- 'png-image': {
115
- id: 'png-image',
116
- originalName: 'image.png',
117
- originalPath: '/path/to/image.png',
118
- originalSize: 100 * 1024,
119
- finalSize: 100 * 1024,
120
- totalCompressionRatio: 0,
121
- type: 'texture',
122
- processingHistory: [
123
- {
124
- stage: 'base-build',
125
- name: 'image.png',
126
- size: 100 * 1024,
127
- optimizations: [],
128
- },
129
- ],
130
- },
131
- },
132
- stages: [],
133
- };
134
- const analyzer = new OptimizationAnalyzer(state);
135
- const result = analyzer.analyze();
136
- expect(result.bySeverity.high.length).toBeGreaterThan(0);
137
- expect(result.bySeverity.medium.length).toBeGreaterThan(0);
138
- });
139
- it('should calculate total estimated savings', () => {
140
- const state = {
141
- version: '1.0.0',
142
- buildTime: Date.now(),
143
- assets: {
144
- 'large-file': {
145
- id: 'large-file',
146
- originalName: 'large-file.js',
147
- originalPath: '/path/to/large-file.js',
148
- originalSize: 200 * 1024,
149
- finalSize: 195 * 1024,
150
- totalCompressionRatio: 0.025,
151
- type: 'script',
152
- processingHistory: [
153
- {
154
- stage: 'base-build',
155
- name: 'large-file.js',
156
- size: 195 * 1024,
157
- optimizations: [],
158
- },
159
- ],
160
- },
161
- },
162
- stages: [],
163
- };
164
- const analyzer = new OptimizationAnalyzer(state);
165
- const result = analyzer.analyze();
166
- expect(result.totalEstimatedSavings).toBeGreaterThan(0);
167
- expect(result.estimatedOptimizationRate).toBeGreaterThan(0);
168
- });
169
- });
@@ -1,100 +0,0 @@
1
- pc.script.createLoadingScreen((app) => {
2
- const createCss = () => {
3
- const css = `
4
- body {
5
- background-color: #283538;
6
- }
7
-
8
- #application-splash-wrapper {
9
- position: absolute;
10
- top: 0;
11
- left: 0;
12
- height: 100%;
13
- width: 100%;
14
- background-color: #283538;
15
- }
16
-
17
- #application-splash {
18
- position: absolute;
19
- top: calc(50% - 28px);
20
- width: 264px;
21
- left: calc(50% - 132px);
22
- }
23
-
24
- #application-splash img {
25
- width: 100%;
26
- }
27
-
28
- #progress-bar-container {
29
- margin: 20px auto 0 auto;
30
- height: 2px;
31
- width: 100%;
32
- background-color: #1d292c;
33
- }
34
-
35
- #progress-bar {
36
- width: 0%;
37
- height: 100%;
38
- background-color: #f60;
39
- }
40
-
41
- @media (max-width: 480px) {
42
- #application-splash {
43
- width: 170px;
44
- left: calc(50% - 85px);
45
- }
46
- }
47
- `;
48
-
49
- const style = document.createElement('style');
50
- style.textContent = css;
51
- document.head.appendChild(style);
52
- };
53
-
54
- const showSplash = () => {
55
- const wrapper = document.createElement('div');
56
- wrapper.id = 'application-splash-wrapper';
57
- document.body.appendChild(wrapper);
58
-
59
- const splash = document.createElement('div');
60
- splash.id = 'application-splash';
61
- wrapper.appendChild(splash);
62
- splash.style.display = 'none';
63
-
64
- const logo = document.createElement('img');
65
- logo.src = `${ASSET_PREFIX}logo.png`;
66
- splash.appendChild(logo);
67
- logo.onload = () => {
68
- splash.style.display = 'block';
69
- };
70
-
71
- const container = document.createElement('div');
72
- container.id = 'progress-bar-container';
73
- splash.appendChild(container);
74
-
75
- const bar = document.createElement('div');
76
- bar.id = 'progress-bar';
77
- container.appendChild(bar);
78
- };
79
-
80
- const setProgress = (value) => {
81
- const bar = document.getElementById('progress-bar');
82
- if (bar) {
83
- value = Math.min(1, Math.max(0, value));
84
- bar.style.width = `${value * 100}%`;
85
- }
86
- };
87
-
88
- const hideSplash = () => {
89
- document.getElementById('application-splash-wrapper').remove();
90
- };
91
-
92
- createCss();
93
- showSplash();
94
-
95
- app.on('preload:end', () => {
96
- app.off('preload:progress');
97
- });
98
- app.on('preload:progress', setProgress);
99
- app.on('start', hideSplash);
100
- });
@@ -1,47 +0,0 @@
1
- var loadModules = function (modules, urlPrefix, doneCallback) { // eslint-disable-line no-unused-vars
2
-
3
- if (typeof modules === "undefined" || modules.length === 0) {
4
- // caller may depend on callback behaviour being async
5
- setTimeout(doneCallback);
6
- } else {
7
- let remaining = modules.length;
8
- const moduleLoaded = () => {
9
- if (--remaining === 0) {
10
- doneCallback();
11
- }
12
- };
13
-
14
- modules.forEach(function (m) {
15
- pc.WasmModule.setConfig(m.moduleName, {
16
- glueUrl: urlPrefix + m.glueUrl,
17
- wasmUrl: urlPrefix + m.wasmUrl,
18
- fallbackUrl: urlPrefix + m.fallbackUrl
19
- });
20
-
21
- if (!m.hasOwnProperty('preload') || m.preload) {
22
- if (m.moduleName === 'BASIS') {
23
- // preload basis transcoder
24
- pc.basisInitialize();
25
- moduleLoaded();
26
- } else if (m.moduleName === 'DracoDecoderModule') {
27
- // preload draco decoder
28
- if (pc.dracoInitialize) {
29
- // 1.63 onwards
30
- pc.dracoInitialize();
31
- moduleLoaded();
32
- } else {
33
- // 1.62 and earlier
34
- pc.WasmModule.getInstance(m.moduleName, () => { moduleLoaded(); });
35
- }
36
- } else {
37
- // load remaining modules in global scope
38
- pc.WasmModule.getInstance(m.moduleName, () => { moduleLoaded(); });
39
- }
40
- } else {
41
- moduleLoaded();
42
- }
43
- });
44
- }
45
- };
46
-
47
- window.loadModules = loadModules;
@@ -1,20 +0,0 @@
1
- window.ASSET_PREFIX = "";
2
- window.SCRIPT_PREFIX = "";
3
- window.SCENE_PATH = "{{SCENE_PATH}}";
4
- window.CONTEXT_OPTIONS = {
5
- 'antialias': {{ANTIALIAS}},
6
- 'alpha': false,
7
- 'preserveDrawingBuffer': {{PRESERVE_DRAWING_BUFFER}},
8
- 'deviceTypes': [`webgl2`, `webgl1`],
9
- 'powerPreference': "{{POWER_PREFERENCE}}"
10
- };
11
- window.SCRIPTS = {{SCRIPTS}};
12
- window.CONFIG_FILENAME = "config.json";
13
- window.INPUT_SETTINGS = {
14
- useKeyboard: {{USE_KEYBOARD}},
15
- useMouse: {{USE_MOUSE}},
16
- useGamepads: {{USE_GAMEPAD}},
17
- useTouch: {{USE_TOUCH}}
18
- };
19
- pc.script.legacy = {{USE_LEGACY_SCRIPTS}};
20
- window.PRELOAD_MODULES = {{PRELOAD_MODULES}};