@playcraft/cli 0.0.1

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 (37) hide show
  1. package/README.md +12 -0
  2. package/dist/build-config.js +26 -0
  3. package/dist/commands/build.js +363 -0
  4. package/dist/commands/config.js +133 -0
  5. package/dist/commands/init.js +86 -0
  6. package/dist/commands/inspect.js +209 -0
  7. package/dist/commands/logs.js +121 -0
  8. package/dist/commands/start.js +284 -0
  9. package/dist/commands/status.js +106 -0
  10. package/dist/commands/stop.js +58 -0
  11. package/dist/config.js +31 -0
  12. package/dist/fs-handler.js +83 -0
  13. package/dist/index.js +200 -0
  14. package/dist/logger.js +122 -0
  15. package/dist/playable/base-builder.js +265 -0
  16. package/dist/playable/builder.js +1462 -0
  17. package/dist/playable/converter.js +150 -0
  18. package/dist/playable/index.js +3 -0
  19. package/dist/playable/platforms/base.js +12 -0
  20. package/dist/playable/platforms/facebook.js +37 -0
  21. package/dist/playable/platforms/index.js +24 -0
  22. package/dist/playable/platforms/snapchat.js +59 -0
  23. package/dist/playable/playable-builder.js +521 -0
  24. package/dist/playable/types.js +1 -0
  25. package/dist/playable/vite/config-builder.js +136 -0
  26. package/dist/playable/vite/platform-configs.js +102 -0
  27. package/dist/playable/vite/plugin-model-compression.js +63 -0
  28. package/dist/playable/vite/plugin-platform.js +65 -0
  29. package/dist/playable/vite/plugin-playcanvas.js +454 -0
  30. package/dist/playable/vite-builder.js +125 -0
  31. package/dist/port-utils.js +27 -0
  32. package/dist/process-manager.js +96 -0
  33. package/dist/server.js +128 -0
  34. package/dist/socket.js +117 -0
  35. package/dist/watcher.js +33 -0
  36. package/package.json +41 -0
  37. package/templates/playable-ad.html +59 -0
@@ -0,0 +1,150 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import mimeTypes from 'mime-types';
4
+ export class OnePageConverter {
5
+ projectDir;
6
+ options;
7
+ assets = [];
8
+ constructor(projectDir, options) {
9
+ this.projectDir = projectDir;
10
+ this.options = options;
11
+ }
12
+ /**
13
+ * 添加资产到转换列表
14
+ */
15
+ addAsset(assetPath, type) {
16
+ this.assets.push({
17
+ path: assetPath,
18
+ type,
19
+ size: 0,
20
+ });
21
+ }
22
+ /**
23
+ * 编码所有资产为 Base64
24
+ */
25
+ async encodeAssets() {
26
+ for (const asset of this.assets) {
27
+ const fullPath = path.join(this.projectDir, asset.path);
28
+ try {
29
+ const fileBuffer = await fs.readFile(fullPath);
30
+ asset.size = fileBuffer.length;
31
+ asset.base64 = fileBuffer.toString('base64');
32
+ asset.mimeType = mimeTypes.lookup(asset.path) || 'application/octet-stream';
33
+ }
34
+ catch (error) {
35
+ console.warn(`Warning: Failed to encode asset ${asset.path}: ${error.message}`);
36
+ }
37
+ }
38
+ }
39
+ /**
40
+ * 获取资产的 Base64 数据 URL
41
+ */
42
+ getDataUrl(assetPath) {
43
+ if (!assetPath)
44
+ return null;
45
+ // 移除查询参数
46
+ let cleanPath = assetPath;
47
+ if (cleanPath.includes('?')) {
48
+ cleanPath = cleanPath.split('?')[0];
49
+ }
50
+ // 解码 URL 编码的路径
51
+ try {
52
+ cleanPath = decodeURIComponent(cleanPath);
53
+ }
54
+ catch (e) {
55
+ // 如果解码失败,使用原始路径
56
+ }
57
+ // 1. 精确匹配
58
+ let asset = this.assets.find(a => a.path === cleanPath);
59
+ // 2. 尝试移除前导斜杠
60
+ if (!asset && cleanPath.startsWith('/')) {
61
+ asset = this.assets.find(a => a.path === cleanPath.substring(1));
62
+ }
63
+ // 3. 尝试模糊匹配(文件名匹配)
64
+ if (!asset) {
65
+ const filename = path.basename(cleanPath);
66
+ asset = this.assets.find(a => {
67
+ const assetFilename = path.basename(a.path);
68
+ return assetFilename === filename;
69
+ });
70
+ }
71
+ // 4. 尝试部分路径匹配(处理 files/assets/... 格式)
72
+ if (!asset) {
73
+ asset = this.assets.find(a => a.path.endsWith(cleanPath) || cleanPath.endsWith(a.path));
74
+ }
75
+ if (!asset || !asset.base64) {
76
+ return null;
77
+ }
78
+ return `data:${asset.mimeType};base64,${asset.base64}`;
79
+ }
80
+ /**
81
+ * 替换 HTML 中的 URL 为 Base64 数据 URL
82
+ */
83
+ replaceUrlsWithBase64(html, urlMap) {
84
+ let result = html;
85
+ for (const [url, dataUrl] of urlMap.entries()) {
86
+ // 替换各种可能的 URL 格式
87
+ result = result.replace(new RegExp(url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), dataUrl);
88
+ // 也替换相对路径
89
+ const relativeUrl = url.startsWith('/') ? url.substring(1) : url;
90
+ result = result.replace(new RegExp(relativeUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), dataUrl);
91
+ }
92
+ return result;
93
+ }
94
+ /**
95
+ * 内联脚本到 HTML
96
+ */
97
+ async inlineScripts(html) {
98
+ // 查找所有 <script src="..."> 标签
99
+ const scriptRegex = /<script\s+src=["']([^"']+)["'][^>]*><\/script>/gi;
100
+ let result = html;
101
+ const matches = [];
102
+ let match;
103
+ // 先收集所有匹配项
104
+ while ((match = scriptRegex.exec(html)) !== null) {
105
+ matches.push({
106
+ fullMatch: match[0],
107
+ scriptPath: match[1],
108
+ });
109
+ }
110
+ // 处理每个脚本
111
+ for (const { fullMatch, scriptPath } of matches) {
112
+ // 尝试多个可能的路径
113
+ const possiblePaths = [
114
+ path.join(this.projectDir, scriptPath),
115
+ path.join(this.projectDir, scriptPath.replace(/^\//, '')),
116
+ scriptPath, // 绝对路径
117
+ ];
118
+ let scriptContent = null;
119
+ for (const fullPath of possiblePaths) {
120
+ try {
121
+ scriptContent = await fs.readFile(fullPath, 'utf-8');
122
+ break;
123
+ }
124
+ catch (error) {
125
+ // 继续尝试下一个路径
126
+ }
127
+ }
128
+ if (scriptContent) {
129
+ // 替换为内联脚本
130
+ result = result.replace(fullMatch, `<script>${scriptContent}</script>`);
131
+ }
132
+ else {
133
+ console.warn(`Warning: Failed to inline script ${scriptPath}`);
134
+ }
135
+ }
136
+ return result;
137
+ }
138
+ /**
139
+ * 获取所有资产信息
140
+ */
141
+ getAssets() {
142
+ return this.assets;
143
+ }
144
+ /**
145
+ * 获取总大小
146
+ */
147
+ getTotalSize() {
148
+ return this.assets.reduce((sum, asset) => sum + asset.size, 0);
149
+ }
150
+ }
@@ -0,0 +1,3 @@
1
+ export { PlayableBuilder } from './builder.js';
2
+ export { OnePageConverter } from './converter.js';
3
+ export { createPlatformAdapter } from './platforms/index.js';
@@ -0,0 +1,12 @@
1
+ export class PlatformAdapter {
2
+ options;
3
+ constructor(options) {
4
+ this.options = options;
5
+ }
6
+ /**
7
+ * 验证配置
8
+ */
9
+ validateOptions() {
10
+ // 默认实现,子类可以覆盖
11
+ }
12
+ }
@@ -0,0 +1,37 @@
1
+ import { PlatformAdapter } from './base.js';
2
+ export class FacebookAdapter extends PlatformAdapter {
3
+ getName() {
4
+ return 'Facebook';
5
+ }
6
+ getSizeLimit() {
7
+ // Facebook: HTML 2MB, ZIP 5MB
8
+ return this.options.format === 'zip' ? 5 * 1024 * 1024 : 2 * 1024 * 1024;
9
+ }
10
+ getDefaultFormat() {
11
+ return 'html';
12
+ }
13
+ modifyHTML(html, assets) {
14
+ // Facebook 需要 FbPlayableAd API
15
+ const fbScript = `
16
+ <script>
17
+ window.FbPlayableAd = window.FbPlayableAd || {
18
+ onCTAClick: function() {
19
+ console.log('Facebook CTA clicked');
20
+ // 这里会被 Facebook 替换为实际的跟踪代码
21
+ }
22
+ };
23
+ </script>
24
+ `;
25
+ // 在 </head> 之前插入
26
+ return html.replace('</head>', `${fbScript}</head>`);
27
+ }
28
+ getPlatformScript() {
29
+ return `
30
+ // Facebook Playable Ad API
31
+ if (typeof FbPlayableAd !== 'undefined') {
32
+ // 在 CTA 按钮点击时调用
33
+ // FbPlayableAd.onCTAClick();
34
+ }
35
+ `;
36
+ }
37
+ }
@@ -0,0 +1,24 @@
1
+ import { FacebookAdapter } from './facebook.js';
2
+ import { SnapchatAdapter } from './snapchat.js';
3
+ export function createPlatformAdapter(options) {
4
+ switch (options.platform) {
5
+ case 'facebook':
6
+ return new FacebookAdapter(options);
7
+ case 'snapchat':
8
+ return new SnapchatAdapter(options);
9
+ case 'ironsource':
10
+ // TODO: 实现 IronSource 适配器
11
+ throw new Error('IronSource adapter not yet implemented');
12
+ case 'applovin':
13
+ // TODO: 实现 AppLovin 适配器
14
+ throw new Error('AppLovin adapter not yet implemented');
15
+ case 'google':
16
+ // TODO: 实现 Google 适配器
17
+ throw new Error('Google adapter not yet implemented');
18
+ default:
19
+ throw new Error(`Unsupported platform: ${options.platform}`);
20
+ }
21
+ }
22
+ export { PlatformAdapter } from './base.js';
23
+ export { FacebookAdapter } from './facebook.js';
24
+ export { SnapchatAdapter } from './snapchat.js';
@@ -0,0 +1,59 @@
1
+ import { PlatformAdapter } from './base.js';
2
+ export class SnapchatAdapter extends PlatformAdapter {
3
+ getName() {
4
+ return 'Snapchat';
5
+ }
6
+ getSizeLimit() {
7
+ // Snapchat: 5MB (uncompressed)
8
+ return 5 * 1024 * 1024;
9
+ }
10
+ getDefaultFormat() {
11
+ return 'zip';
12
+ }
13
+ modifyHTML(html, assets) {
14
+ // Snapchat 需要 MRAID 2.0 和 snapchatCta 函数
15
+ const mraidScript = `
16
+ <script>
17
+ // MRAID 2.0 API
18
+ window.mraid = window.mraid || {
19
+ getVersion: function() { return '2.0'; },
20
+ isReady: function() { return true; },
21
+ open: function(url) { window.open(url); },
22
+ close: function() { window.close(); },
23
+ addEventListener: function(event, listener) {},
24
+ removeEventListener: function(event, listener) {},
25
+ getState: function() { return 'ready'; },
26
+ getPlacementType: function() { return 'interstitial'; }
27
+ };
28
+
29
+ // Snapchat CTA function
30
+ window.snapchatCta = function() {
31
+ if (window.mraid && window.mraid.open) {
32
+ window.mraid.open('https://snapchat.com');
33
+ }
34
+ };
35
+ </script>
36
+ `;
37
+ return html.replace('</head>', `${mraidScript}</head>`);
38
+ }
39
+ getPlatformScript() {
40
+ return `
41
+ // Snapchat Playable Ad
42
+ if (typeof mraid !== 'undefined') {
43
+ mraid.addEventListener('ready', function() {
44
+ console.log('MRAID ready');
45
+ });
46
+ }
47
+
48
+ // 在 CTA 按钮点击时调用
49
+ // snapchatCta();
50
+ `;
51
+ }
52
+ validateOptions() {
53
+ // Snapchat 默认使用 ZIP 格式,但不强制要求
54
+ // MRAID 支持会在 modifyHTML 中自动添加
55
+ if (this.options.format && this.options.format !== 'zip') {
56
+ console.warn('Warning: Snapchat typically uses ZIP format');
57
+ }
58
+ }
59
+ }