@playcraft/cli 0.0.14 → 0.0.17
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.
- package/dist/agent/agent.js +54 -1
- package/dist/agent/fs-backend.js +312 -8
- package/dist/agent/local-backend.js +249 -18
- package/dist/commands/build-all.js +477 -0
- package/dist/commands/build.js +238 -176
- package/dist/fs-handler.js +117 -0
- package/dist/index.js +59 -14
- package/dist/playable/base-builder.js +265 -0
- package/dist/playable/builder.js +1462 -0
- package/dist/playable/converter.js +150 -0
- package/dist/playable/index.js +3 -0
- package/dist/playable/platforms/base.js +12 -0
- package/dist/playable/platforms/facebook.js +37 -0
- package/dist/playable/platforms/index.js +24 -0
- package/dist/playable/platforms/snapchat.js +59 -0
- package/dist/playable/playable-builder.js +521 -0
- package/dist/playable/types.js +1 -0
- package/dist/playable/vite/config-builder.js +136 -0
- package/dist/playable/vite/platform-configs.js +102 -0
- package/dist/playable/vite/plugin-model-compression.js +63 -0
- package/dist/playable/vite/plugin-platform.js +65 -0
- package/dist/playable/vite/plugin-playcanvas.js +454 -0
- package/dist/playable/vite-builder.js +125 -0
- package/dist/server.js +23 -6
- package/dist/socket.js +7 -2
- package/dist/watcher.js +27 -1
- package/package.json +5 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { build as viteBuild } from 'vite';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ViteConfigBuilder } from './vite/config-builder.js';
|
|
5
|
+
import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
|
|
6
|
+
/**
|
|
7
|
+
* Vite 构建器 - 使用 Vite 构建 Playable Ads
|
|
8
|
+
*
|
|
9
|
+
* 职责:
|
|
10
|
+
* 1. 验证输入是有效的基础构建
|
|
11
|
+
* 2. 创建 Vite 配置
|
|
12
|
+
* 3. 执行 Vite 构建
|
|
13
|
+
* 4. 验证输出大小
|
|
14
|
+
* 5. 生成报告
|
|
15
|
+
*/
|
|
16
|
+
export class ViteBuilder {
|
|
17
|
+
baseBuildDir;
|
|
18
|
+
options;
|
|
19
|
+
sizeReport;
|
|
20
|
+
constructor(baseBuildDir, options) {
|
|
21
|
+
this.baseBuildDir = baseBuildDir;
|
|
22
|
+
this.options = options;
|
|
23
|
+
const platformConfig = PLATFORM_CONFIGS[options.platform];
|
|
24
|
+
this.sizeReport = {
|
|
25
|
+
engine: 0,
|
|
26
|
+
assets: {},
|
|
27
|
+
total: 0,
|
|
28
|
+
limit: platformConfig.sizeLimit,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 执行构建
|
|
33
|
+
*/
|
|
34
|
+
async build() {
|
|
35
|
+
// 1. 验证输入
|
|
36
|
+
await this.validateBaseBuild();
|
|
37
|
+
// 2. 创建 Vite 配置
|
|
38
|
+
const configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
|
|
39
|
+
const viteConfig = configBuilder.create();
|
|
40
|
+
// 3. 执行 Vite 构建
|
|
41
|
+
await viteBuild(viteConfig);
|
|
42
|
+
// 4. 验证输出大小
|
|
43
|
+
const outputPath = this.getOutputPath();
|
|
44
|
+
await this.validateSize(outputPath);
|
|
45
|
+
// 5. 生成报告
|
|
46
|
+
this.generateReport(outputPath);
|
|
47
|
+
return outputPath;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 验证基础构建
|
|
51
|
+
*/
|
|
52
|
+
async validateBaseBuild() {
|
|
53
|
+
const requiredFiles = [
|
|
54
|
+
'index.html',
|
|
55
|
+
'config.json',
|
|
56
|
+
'__start__.js',
|
|
57
|
+
];
|
|
58
|
+
const missingFiles = [];
|
|
59
|
+
for (const file of requiredFiles) {
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(path.join(this.baseBuildDir, file));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
missingFiles.push(file);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (missingFiles.length > 0) {
|
|
68
|
+
throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
69
|
+
`请确保输入目录包含完整的多文件构建产物。`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 获取输出路径
|
|
74
|
+
*/
|
|
75
|
+
getOutputPath() {
|
|
76
|
+
const platformConfig = PLATFORM_CONFIGS[this.options.platform];
|
|
77
|
+
const outputDir = this.options.outputDir || './dist';
|
|
78
|
+
if (platformConfig.outputFormat === 'zip') {
|
|
79
|
+
return path.join(outputDir, 'playable.zip');
|
|
80
|
+
}
|
|
81
|
+
return path.join(outputDir, platformConfig.outputFileName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 验证输出大小
|
|
85
|
+
*/
|
|
86
|
+
async validateSize(outputPath) {
|
|
87
|
+
try {
|
|
88
|
+
const stats = await fs.stat(outputPath);
|
|
89
|
+
this.sizeReport.total = stats.size;
|
|
90
|
+
this.sizeReport.assets[path.basename(outputPath)] = stats.size;
|
|
91
|
+
if (stats.size > this.sizeReport.limit) {
|
|
92
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
93
|
+
const limitMB = (this.sizeReport.limit / 1024 / 1024).toFixed(2);
|
|
94
|
+
console.warn(`⚠️ 警告: 文件大小 ${sizeMB} MB 超过限制 ${limitMB} MB`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn(`警告: 无法读取输出文件: ${outputPath}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 生成报告
|
|
103
|
+
*/
|
|
104
|
+
generateReport(outputPath) {
|
|
105
|
+
// 报告已在 validateSize 中生成
|
|
106
|
+
// 这里可以添加额外的报告逻辑
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 获取大小报告
|
|
110
|
+
*/
|
|
111
|
+
getSizeReport() {
|
|
112
|
+
return this.sizeReport;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 格式化字节数
|
|
116
|
+
*/
|
|
117
|
+
formatBytes(bytes) {
|
|
118
|
+
if (bytes === 0)
|
|
119
|
+
return '0 Bytes';
|
|
120
|
+
const k = 1024;
|
|
121
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
122
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
123
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import cors from 'cors';
|
|
3
|
-
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export function createServer(config, fsHandler, options) {
|
|
4
5
|
const app = express();
|
|
5
|
-
app.use(cors(
|
|
6
|
-
|
|
6
|
+
app.use(cors({
|
|
7
|
+
origin: true, // reflect request origin
|
|
8
|
+
credentials: true, // allow credentials (withCredentials)
|
|
9
|
+
}));
|
|
10
|
+
app.use(express.json({ limit: '50mb' }));
|
|
7
11
|
// Health check
|
|
8
12
|
app.get('/health', (req, res) => {
|
|
9
13
|
res.json({ status: 'ok', projectId: config.projectId });
|
|
@@ -54,10 +58,22 @@ export function createServer(config, fsHandler) {
|
|
|
54
58
|
return res.status(400).json({ error: 'Missing path parameter' });
|
|
55
59
|
}
|
|
56
60
|
try {
|
|
57
|
-
|
|
61
|
+
let result = await fsHandler.readBinaryFile(filePath);
|
|
62
|
+
let actualPath = filePath;
|
|
63
|
+
// Fallback: if exact path not found and path starts with "assets/",
|
|
64
|
+
// search recursively in the assets directory for a file with the same name.
|
|
65
|
+
// Note: findFileRecursive() has built-in caching (5min TTL) and depth limiting (max 10 levels)
|
|
66
|
+
if (!result.exists && filePath.startsWith('assets/')) {
|
|
67
|
+
const fileName = path.basename(filePath);
|
|
68
|
+
const found = await fsHandler.findFileRecursive('assets', fileName);
|
|
69
|
+
if (found) {
|
|
70
|
+
result = await fsHandler.readBinaryFile(found);
|
|
71
|
+
actualPath = found;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
58
74
|
if (result.exists) {
|
|
59
|
-
// Set appropriate content type
|
|
60
|
-
const ext =
|
|
75
|
+
// Set appropriate content type based on the actual file path
|
|
76
|
+
const ext = actualPath.split('.').pop()?.toLowerCase();
|
|
61
77
|
const contentType = getContentType(ext || '');
|
|
62
78
|
res.setHeader('Content-Type', contentType);
|
|
63
79
|
if (result.size !== undefined) {
|
|
@@ -81,6 +97,7 @@ export function createServer(config, fsHandler) {
|
|
|
81
97
|
}
|
|
82
98
|
try {
|
|
83
99
|
await fsHandler.writeFile(filePath, content ?? '');
|
|
100
|
+
options?.onFileWritten?.(filePath);
|
|
84
101
|
res.json({ success: true, path: filePath });
|
|
85
102
|
}
|
|
86
103
|
catch (error) {
|
package/dist/socket.js
CHANGED
|
@@ -59,8 +59,9 @@ export class SocketServer {
|
|
|
59
59
|
onConnectionChange;
|
|
60
60
|
constructor(server, config, onConnectionChange) {
|
|
61
61
|
this.config = config;
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
// 使用 noServer 模式,由 agent.ts 的统一 upgrade handler 分发 WebSocket 连接
|
|
63
|
+
// 避免多个 WebSocketServer 在同一个 HTTP server 上的 upgrade 事件冲突
|
|
64
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
64
65
|
this.onConnectionChange = onConnectionChange;
|
|
65
66
|
this.wss.on('connection', (ws) => {
|
|
66
67
|
this.clients.set(ws, { patterns: ['**/*'] });
|
|
@@ -173,4 +174,8 @@ export class SocketServer {
|
|
|
173
174
|
getConnectionCount() {
|
|
174
175
|
return this.clients.size;
|
|
175
176
|
}
|
|
177
|
+
/** 获取内部 WebSocketServer 实例,用于手动 handleUpgrade */
|
|
178
|
+
getWss() {
|
|
179
|
+
return this.wss;
|
|
180
|
+
}
|
|
176
181
|
}
|
package/dist/watcher.js
CHANGED
|
@@ -4,6 +4,13 @@ export class Watcher {
|
|
|
4
4
|
config;
|
|
5
5
|
onChange;
|
|
6
6
|
watcher;
|
|
7
|
+
/**
|
|
8
|
+
* 被抑制的文件路径及其过期时间戳。
|
|
9
|
+
* 当 Editor 通过 HTTP 保存文件时,将文件路径加入此 Map,
|
|
10
|
+
* Watcher 在窗口期内忽略该文件的变更通知,防止保存→watcher→reload 循环。
|
|
11
|
+
*/
|
|
12
|
+
suppressedFiles = new Map();
|
|
13
|
+
static SUPPRESS_WINDOW_MS = 3000; // 3 秒抑制窗口
|
|
7
14
|
constructor(config, onChange) {
|
|
8
15
|
this.config = config;
|
|
9
16
|
this.onChange = onChange;
|
|
@@ -24,9 +31,28 @@ export class Watcher {
|
|
|
24
31
|
.on('unlink', (filePath) => this.handleEvent(filePath, 'delete'));
|
|
25
32
|
}
|
|
26
33
|
handleEvent(filePath, type) {
|
|
27
|
-
|
|
34
|
+
// 统一使用 / 分隔符,确保 Windows 和 Unix 行为一致
|
|
35
|
+
const relativePath = path.relative(this.config.dir, filePath).replace(/\\/g, '/');
|
|
36
|
+
// 检查是否在抑制窗口内
|
|
37
|
+
const suppressUntil = this.suppressedFiles.get(relativePath);
|
|
38
|
+
if (suppressUntil && Date.now() < suppressUntil) {
|
|
39
|
+
// 文件刚被 Editor 保存,忽略 Watcher 回弹
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// 过期清理
|
|
43
|
+
if (suppressUntil) {
|
|
44
|
+
this.suppressedFiles.delete(relativePath);
|
|
45
|
+
}
|
|
28
46
|
this.onChange(relativePath, type);
|
|
29
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 抑制指定文件的 Watcher 通知(在窗口期内)。
|
|
50
|
+
* 应在 Editor 通过 HTTP API 保存文件后调用。
|
|
51
|
+
*/
|
|
52
|
+
suppressFile(relativePath, durationMs) {
|
|
53
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
54
|
+
this.suppressedFiles.set(normalizedPath, Date.now() + (durationMs || Watcher.SUPPRESS_WINDOW_MS));
|
|
55
|
+
}
|
|
30
56
|
close() {
|
|
31
57
|
return this.watcher.close();
|
|
32
58
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"playcraft": "./dist/index.js"
|
|
7
|
+
"playcraft": "./dist/index.js",
|
|
8
|
+
"playcraft-test": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|
|
@@ -21,8 +22,8 @@
|
|
|
21
22
|
"release": "node scripts/release.js"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@playcraft/common": "^0.0.
|
|
25
|
-
"@playcraft/build": "^0.0.
|
|
25
|
+
"@playcraft/common": "^0.0.7",
|
|
26
|
+
"@playcraft/build": "^0.0.14",
|
|
26
27
|
"chokidar": "^4.0.3",
|
|
27
28
|
"commander": "^13.1.0",
|
|
28
29
|
"cors": "^2.8.6",
|