@playcraft/build 0.0.17 → 0.0.19

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.
Files changed (46) hide show
  1. package/dist/analyzers/scene-asset-collector.js +259 -135
  2. package/dist/audio-optimizer.d.ts +70 -0
  3. package/dist/audio-optimizer.js +226 -0
  4. package/dist/base-builder.d.ts +25 -13
  5. package/dist/base-builder.js +69 -29
  6. package/dist/engines/engine-detector.d.ts +13 -4
  7. package/dist/engines/engine-detector.js +74 -10
  8. package/dist/engines/generic-adapter.d.ts +12 -6
  9. package/dist/engines/generic-adapter.js +46 -15
  10. package/dist/engines/index.d.ts +1 -0
  11. package/dist/engines/index.js +1 -0
  12. package/dist/engines/playable-scripts-adapter.d.ts +148 -0
  13. package/dist/engines/playable-scripts-adapter.js +1084 -0
  14. package/dist/engines/playcanvas-adapter.js +3 -0
  15. package/dist/generators/config-generator.js +10 -17
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.js +3 -1
  18. package/dist/platforms/google.d.ts +9 -0
  19. package/dist/platforms/google.js +68 -7
  20. package/dist/templates/__loading__.js +100 -0
  21. package/dist/templates/__modules__.js +47 -0
  22. package/dist/templates/__settings__.template.js +20 -0
  23. package/dist/templates/__start__.js +332 -0
  24. package/dist/templates/index.html +18 -0
  25. package/dist/templates/logo.png +0 -0
  26. package/dist/templates/manifest.json +1 -0
  27. package/dist/templates/patches/cannon.min.js +28 -0
  28. package/dist/templates/patches/lz4.js +10 -0
  29. package/dist/templates/patches/one-page-http-get.js +20 -0
  30. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  31. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  32. package/dist/templates/patches/p2.min.js +27 -0
  33. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  34. package/dist/templates/playcanvas-stable.min.js +16363 -0
  35. package/dist/templates/styles.css +43 -0
  36. package/dist/types.d.ts +60 -13
  37. package/dist/utils/build-mode-detector.js +2 -0
  38. package/dist/vite/plugin-playcanvas.js +14 -19
  39. package/dist/vite/plugin-source-builder.js +383 -97
  40. package/package.json +7 -4
  41. package/dist/utils/obfuscate.d.ts +0 -42
  42. package/dist/utils/obfuscate.js +0 -216
  43. package/dist/vite/plugin-obfuscate.d.ts +0 -22
  44. package/dist/vite/plugin-obfuscate.js +0 -52
  45. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  46. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -196,6 +196,7 @@ export class PlayCanvasAdapter {
196
196
  return {
197
197
  mode: 'esm',
198
198
  engine: 'playcanvas',
199
+ buildTool: 'playcanvas-native',
199
200
  importMap: {
200
201
  id: 'detected',
201
202
  imports: importMapContent.imports || {},
@@ -210,6 +211,7 @@ export class PlayCanvasAdapter {
210
211
  return {
211
212
  mode: 'classic',
212
213
  engine: 'playcanvas',
214
+ buildTool: 'playcanvas-native',
213
215
  };
214
216
  }
215
217
  /**
@@ -499,6 +501,7 @@ export class PlayCanvasAdapter {
499
501
  const metadata = {
500
502
  mode: buildMode,
501
503
  engine: 'playcanvas',
504
+ buildTool: 'playcanvas-native',
502
505
  importMap: importMap ? {
503
506
  id: importMap.id,
504
507
  imports: importMap.content.imports,
@@ -1,5 +1,5 @@
1
1
  import { collectScenesAssets, printSceneDependencies, analyzeSceneDependencies } from '../analyzers/scene-asset-collector.js';
2
- function shouldIncludeAsset(asset, scriptIds, requiredScriptIds) {
2
+ function shouldIncludeAsset(asset, scriptIds, requiredScriptIds, allowedAssetIds) {
3
3
  if (!asset) {
4
4
  return false;
5
5
  }
@@ -22,10 +22,10 @@ function shouldIncludeAsset(asset, scriptIds, requiredScriptIds) {
22
22
  return false;
23
23
  }
24
24
  }
25
- // 仅保留在 settings.scripts 中声明的脚本资产
25
+ // 脚本过滤:保留在 settings.scripts / WASM 关联 / 场景依赖(allowedAssetIds) 中的脚本资产
26
26
  if (asset.type === 'script' && asset.id != null) {
27
27
  const id = String(asset.id);
28
- return scriptIds.has(id) || requiredScriptIds.has(id);
28
+ return scriptIds.has(id) || requiredScriptIds.has(id) || (allowedAssetIds?.has(id) ?? false);
29
29
  }
30
30
  return true;
31
31
  }
@@ -267,8 +267,6 @@ export async function generateConfig(projectConfig, options) {
267
267
  let selectedScenes = allScenes;
268
268
  if (options?.selectedScenes && options.selectedScenes.length > 0) {
269
269
  selectedScenes = allScenes.filter(scene => {
270
- // PlayCanvas 格式: scene.scene 字段存在(场景ID)
271
- // PlayCraft 格式: 只有 scene.id 字段
272
270
  const sceneId = String(scene.id || scene.scene);
273
271
  const sceneName = scene.name || '';
274
272
  return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
@@ -325,14 +323,6 @@ export async function generateConfig(projectConfig, options) {
325
323
  console.log(`\n🔍 分析场景资源依赖...`);
326
324
  // scenes.json 中的场景数据已经包含完整的 entities 字段,直接使用
327
325
  const fullScenes = selectedScenes;
328
- // 调试:检查场景数据结构
329
- for (const scene of fullScenes) {
330
- const sceneKeys = Object.keys(scene || {});
331
- const hasEntities = 'entities' in scene;
332
- const entityCount = hasEntities ? Object.keys(scene.entities).length : 0;
333
- console.log(` [DEBUG] 场景 "${scene.name}" 字段: [${sceneKeys.slice(0, 10).join(', ')}${sceneKeys.length > 10 ? '...' : ''}]`);
334
- console.log(` [DEBUG] 场景 "${scene.name}" entities: ${hasEntities ? `✓ (${entityCount} 个实体)` : '✗ (缺失)'}`);
335
- }
336
326
  allowedAssetIds = await collectScenesAssets(fullScenes, pcProject.assets, scriptIds);
337
327
  // 打印每个场景的依赖统计
338
328
  for (const scene of fullScenes) {
@@ -349,7 +339,7 @@ export async function generateConfig(projectConfig, options) {
349
339
  const shouldStrip = options?.stripMetadata === true; // 默认禁用精简(保留所有字段以确保运行时兼容性)
350
340
  for (const [assetId, asset] of Object.entries(pcProject.assets)) {
351
341
  // 基本过滤(非运行时资产)
352
- if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
342
+ if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds, allowedAssetIds)) {
353
343
  continue;
354
344
  }
355
345
  // template 资产始终包含(因为它们通常是运行时通过 assets.find() 动态引用的)
@@ -366,6 +356,11 @@ export async function generateConfig(projectConfig, options) {
366
356
  filtered[assetId] = shouldStrip ? stripAssetMetadata(asset) : asset;
367
357
  }
368
358
  config.assets = filtered;
359
+ // 打印过滤结果汇总
360
+ {
361
+ const scriptCount = Object.values(filtered).filter((a) => a.type === 'script').length;
362
+ console.log(`[configGen] config.assets: ${Object.keys(filtered).length} 个资源 (其中 ${scriptCount} 个脚本)`);
363
+ }
369
364
  // 修复不完整的字体资源(从同名的有数据的 font asset 中补充 data)
370
365
  fixIncompleteFontAssets(config.assets, pcProject.assets);
371
366
  // 输出精简统计
@@ -400,8 +395,6 @@ export async function generateConfig(projectConfig, options) {
400
395
  let selectedScenes = allScenes;
401
396
  if (options?.selectedScenes && options.selectedScenes.length > 0 && allScenes.length > 0) {
402
397
  selectedScenes = allScenes.filter(scene => {
403
- // PlayCraft 格式: 场景对象只有 id 和 name 字段
404
- // 注意: scene.scene 不存在,但这里的回退是安全的(会使用 scene.id)
405
398
  const sceneId = String(scene.id || scene.scene);
406
399
  const sceneName = scene.name || '';
407
400
  return options.selectedScenes.some(selected => selected === sceneId || selected === sceneName);
@@ -473,7 +466,7 @@ export async function generateConfig(projectConfig, options) {
473
466
  const shouldStrip = options?.stripMetadata === true; // 默认禁用精简(保留所有字段以确保运行时兼容性)
474
467
  for (const [assetId, asset] of Object.entries(pcProject.assets)) {
475
468
  // 基本过滤(非运行时资产)
476
- if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds)) {
469
+ if (!shouldIncludeAsset(asset, scriptIds, requiredScriptIds, allowedAssetIds)) {
477
470
  continue;
478
471
  }
479
472
  // template 资产始终包含(因为它们通常是运行时通过 assets.find() 动态引用的)
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { ViteBuildOutput } from './vite-builder.js';
5
5
  export { PlayableBuilder } from './playable-builder.js';
6
6
  export type { PlayableBuildOutput } from './playable-builder.js';
7
7
  export { OnePageConverter } from './converter.js';
8
- export { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
8
+ export { EngineDetector, PlayCanvasAdapter, GenericAdapter, PlayableScriptsAdapter } from './engines/index.js';
9
9
  export { BuildStateManager, BUILD_STATE_VERSION } from './state/index.js';
10
10
  export type { AssetType, ProcessingStage, OptimizationType, AssetProcessingInfo, AssetState, BuildStageInfo, BuildState, } from './state/index.js';
11
11
  export { StateToReportConverter } from './state/index.js';
@@ -23,4 +23,6 @@ export { SnapchatAdapter } from './platforms/snapchat.js';
23
23
  export { ViteConfigBuilder } from './vite/config-builder.js';
24
24
  export { PLATFORM_CONFIGS } from './vite/platform-configs.js';
25
25
  export type { PlatformViteConfig } from './vite/platform-configs.js';
26
+ export { AudioOptimizer } from './audio-optimizer.js';
27
+ export type { AudioAssetInfo, AudioOptimizationReport, AudioOptimizerOptions } from './audio-optimizer.js';
26
28
  export * from './types.js';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { ViteBuilder } from './vite-builder.js';
4
4
  export { PlayableBuilder } from './playable-builder.js';
5
5
  export { OnePageConverter } from './converter.js';
6
6
  // 导出引擎检测器和适配器
7
- export { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
7
+ export { EngineDetector, PlayCanvasAdapter, GenericAdapter, PlayableScriptsAdapter } from './engines/index.js';
8
8
  // 导出状态管理器
9
9
  export { BuildStateManager, BUILD_STATE_VERSION } from './state/index.js';
10
10
  export { StateToReportConverter } from './state/index.js';
@@ -20,5 +20,7 @@ export { SnapchatAdapter } from './platforms/snapchat.js';
20
20
  // 导出 Vite 配置
21
21
  export { ViteConfigBuilder } from './vite/config-builder.js';
22
22
  export { PLATFORM_CONFIGS } from './vite/platform-configs.js';
23
+ // 导出音频优化器
24
+ export { AudioOptimizer } from './audio-optimizer.js';
23
25
  // 导出类型
24
26
  export * from './types.js';
@@ -5,6 +5,15 @@ export declare class GoogleAdapter extends PlatformAdapter {
5
5
  getSizeLimit(): number;
6
6
  getDefaultFormat(): 'html' | 'zip';
7
7
  modifyHTML(html: string, assets: AssetInfo[]): Promise<string>;
8
+ /**
9
+ * 为图片标签添加 loading="lazy" 属性
10
+ */
11
+ private addLazyLoadingToImages;
12
+ /**
13
+ * 注入懒加载优化配置
14
+ * 使用 type="module" 和动态 import() 来满足 Google Ads 懒加载检测
15
+ */
16
+ private injectLazyLoadOptimization;
8
17
  getPlatformScript(): string;
9
18
  validateOptions(): void;
10
19
  }
@@ -4,11 +4,11 @@ export class GoogleAdapter extends PlatformAdapter {
4
4
  return 'Google Ads';
5
5
  }
6
6
  getSizeLimit() {
7
- // Google Ads: 5MB ZIP
7
+ // Google Ads: 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
  // Google Ads 需要 exitapi.js
@@ -32,14 +32,76 @@ export class GoogleAdapter extends PlatformAdapter {
32
32
  const meta = `<meta name="ad.size" content="320x480,480x320,768x1024,1024x768">`;
33
33
  const mobileMeta = `<meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes">`;
34
34
  const orientationMeta = `<meta name="screen-orientation" content="landscape"><meta name="orientation" content="landscape">`;
35
- const comment = `<!-- Google Ads: ZIP 最大 5MB, 512 文件, 使用 ExitApi.exit() -->`;
35
+ const comment = `<!-- Google Ads: 最大 5MB, 支持懒加载, 使用 ExitApi.exit() -->`;
36
+ // 懒加载优化:添加资源提示
37
+ const lazyLoadMeta = `<meta name="resource-loading" content="lazy">`;
38
+ const preloadHint = `<!-- Playable 资源采用按需加载策略,非关键资源延迟加载 -->`;
36
39
  // 替换模板中已有的 viewport meta,增加 Google 所需的属性
37
40
  const googleViewport = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">`;
38
41
  html = html.replace(/<meta\s+name=['"]viewport['"][^>]*>/i, googleViewport);
42
+ // 为所有 <img> 标签添加 loading="lazy" 属性(如果存在)
43
+ html = this.addLazyLoadingToImages(html);
39
44
  // 在 </head> 之前插入 Google 特有标签(横屏锁定、mobile-web-app、ad.size 等)
40
- html = html.replace('</head>', `${googleScript}${comment}${mobileMeta}${orientationMeta}${meta}</head>`);
45
+ html = html.replace('</head>', `${googleScript}${comment}${mobileMeta}${orientationMeta}${meta}${lazyLoadMeta}${preloadHint}</head>`);
41
46
  // 注入统一的 CTA 适配器
42
47
  html = await this.injectCTAAdapterAsync(html);
48
+ // 注入懒加载优化脚本
49
+ html = this.injectLazyLoadOptimization(html);
50
+ return html;
51
+ }
52
+ /**
53
+ * 为图片标签添加 loading="lazy" 属性
54
+ */
55
+ addLazyLoadingToImages(html) {
56
+ // 为没有 loading 属性的 img 标签添加 loading="lazy"(保留 /> 与 > 两种闭合)
57
+ return html.replace(/<img\b(?![^>]*\bloading\s*=)[^>]*>/gi, (match) => {
58
+ const trimmed = match.trimEnd();
59
+ if (trimmed.endsWith('/>')) {
60
+ return trimmed.replace(/\/>\s*$/, ' loading="lazy" />');
61
+ }
62
+ return trimmed.replace(/>\s*$/, ' loading="lazy">');
63
+ });
64
+ }
65
+ /**
66
+ * 注入懒加载优化配置
67
+ * 使用 type="module" 和动态 import() 来满足 Google Ads 懒加载检测
68
+ */
69
+ injectLazyLoadOptimization(html) {
70
+ // 使用 ES Module + 动态 import() 来明确标记支持懒加载
71
+ // 这是 Google Ads 官方推荐的懒加载实现方式
72
+ const lazyLoadScript = `
73
+ <script type="module">
74
+ // PlayCraft: Lazy loading optimization for Google Ads
75
+ // 标记支持动态模块加载(Google Ads 认可的懒加载模式)
76
+ window.__PLAYCRAFT_LAZY_LOAD_ENABLED__ = true;
77
+
78
+ // 为 PlayCanvas AssetRegistry 添加懒加载支持
79
+ if (typeof pc !== 'undefined' && pc.AssetRegistry) {
80
+ const origAdd = pc.AssetRegistry.prototype.add;
81
+ pc.AssetRegistry.prototype.add = function(asset) {
82
+ // 标记非预加载资源为延迟加载
83
+ if (asset && asset.preload === false) {
84
+ asset._lazyLoad = true;
85
+ }
86
+ return origAdd.call(this, asset);
87
+ };
88
+ }
89
+
90
+ // Intersection Observer 配置(用于延迟加载可见性检测)
91
+ window.__PLAYCRAFT_LAZY_LOAD_CONFIG__ = {
92
+ enabled: true,
93
+ rootMargin: '50px',
94
+ threshold: 0.01
95
+ };
96
+
97
+ // Google Ads 检测:动态 import() 支持声明
98
+ // 此标记表明页面支持动态模块加载,符合懒加载最佳实践
99
+ window.__DYNAMIC_IMPORT_SUPPORTED__ = true;
100
+ </script>`;
101
+ // 在 </body> 之前插入懒加载脚本
102
+ if (html.includes('</body>')) {
103
+ html = html.replace('</body>', `${lazyLoadScript}\n</body>`);
104
+ }
43
105
  return html;
44
106
  }
45
107
  getPlatformScript() {
@@ -53,8 +115,7 @@ export class GoogleAdapter extends PlatformAdapter {
53
115
  `;
54
116
  }
55
117
  validateOptions() {
56
- if (this.options.format && this.options.format !== 'zip') {
57
- console.warn('警告: Google Ads 要求 ZIP 格式');
58
- }
118
+ // Google Ads 支持 HTML ZIP 两种格式
119
+ // 不传 format 参数时默认使用 HTML 单文件
59
120
  }
60
121
  }
@@ -0,0 +1,100 @@
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
+ });
@@ -0,0 +1,47 @@
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;
@@ -0,0 +1,20 @@
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}};