@playcraft/cli 0.1.64 → 0.1.66
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/README.md +24 -31
- package/dist/commands/build.js +70 -5
- package/dist/commands/create.js +4 -4
- package/dist/commands/inspect.js +4 -6
- package/dist/index.js +27 -19
- package/dist/model-adaptation/node-inspection.js +173 -0
- package/dist/model-adaptation/project-template.js +40 -8
- package/dist/model-adaptation/project.js +2 -2
- package/dist/model-preparation/pipeline-engine.js +157 -0
- package/dist/model-preparation/pipeline-files.js +108 -0
- package/dist/model-preparation/pipeline-worker.js +32 -0
- package/dist/workspace-runtime/batch/deliver.js +2 -2
- package/dist/workspace-runtime/codex/delivery-tools.js +36 -3
- package/dist/workspace-runtime/previews/publish.js +2 -2
- package/dist/workspace-runtime/serve.js +5 -0
- package/dist/workspace-runtime/server/directory-access.js +1 -1
- package/dist/workspace-runtime/server/dispatch.js +27 -47
- package/dist/workspace-runtime/server/http.js +12 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +75 -0
- package/dist/workspace-runtime/workspaces/model-build-inputs.js +27 -0
- package/dist/workspace-runtime/workspaces/model-preparation-store.js +134 -0
- package/dist/workspace-runtime/workspaces/model-preparation.js +357 -0
- package/dist/workspace-runtime/workspaces/platform-export.js +1 -0
- package/package.json +4 -4
- package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- package/project-templates/empty/AGENTS.md +6 -4
- package/project-templates/phaser/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- package/project-templates/threejs/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- package/dist/workspace-runtime/workspaces/delivery-build.js +0 -440
package/README.md
CHANGED
|
@@ -258,16 +258,16 @@ PLAYCRAFT_TOOL_REQUEST_ID=<错误中显示的请求编号> playcraft tools build
|
|
|
258
258
|
|
|
259
259
|
### 构建相关
|
|
260
260
|
|
|
261
|
-
| 命令 | 说明
|
|
262
|
-
| ---------------- |
|
|
263
|
-
| `build` |
|
|
264
|
-
| `build-base` | 仅执行阶段1(生成多文件产物)
|
|
265
|
-
| `build-playable` | 仅执行阶段2(从多文件产物生成 Playable)
|
|
266
|
-
| `analyze` | 生成打包分析报告
|
|
261
|
+
| 命令 | 说明 | 示例 |
|
|
262
|
+
| ---------------- | ---------------------------------------------- | ------------------------------------------------------ |
|
|
263
|
+
| `build` | 本地构建预览,产物为 `dist/preview/index.html` | `playcraft build` |
|
|
264
|
+
| `build-base` | 仅执行阶段1(生成多文件产物) | `playcraft build-base ./src --output ./build` |
|
|
265
|
+
| `build-playable` | 仅执行阶段2(从多文件产物生成 Playable) | `playcraft build-playable ./build --platform facebook` |
|
|
266
|
+
| `analyze` | 生成打包分析报告 | `playcraft analyze --platform facebook` |
|
|
267
267
|
|
|
268
268
|
## 🎯 支持的平台
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
平台导出支持以下主流广告平台(渠道包通过平台导出生成,`playcraft build` 只做本地预览构建):
|
|
271
271
|
|
|
272
272
|
| 平台 | 格式 | 大小限制 | SDK/API |
|
|
273
273
|
| ----------------- | -------- | -------------------- | ------------ |
|
|
@@ -296,45 +296,39 @@ playcraft start --project your-project-id
|
|
|
296
296
|
# 3. 在本地 IDE 编辑代码,云端编辑器自动同步
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
###
|
|
299
|
+
### 本地构建预览
|
|
300
300
|
|
|
301
301
|
```bash
|
|
302
|
-
#
|
|
303
|
-
playcraft build
|
|
304
|
-
|
|
305
|
-
# Snapchat 平台(ZIP)
|
|
306
|
-
playcraft build --platform snapchat --format zip --output ./dist/snapchat
|
|
307
|
-
|
|
308
|
-
# 批量打包所有平台
|
|
309
|
-
for platform in facebook snapchat ironsource applovin google tiktok unity liftoff moloco bigo; do
|
|
310
|
-
playcraft build --platform $platform --output ./dist/$platform
|
|
311
|
-
done
|
|
302
|
+
# 在项目根目录构建,产物固定为 dist/preview/index.html
|
|
303
|
+
playcraft build
|
|
312
304
|
```
|
|
313
305
|
|
|
306
|
+
`playcraft build` 不提供渠道、格式和输出目录参数,各广告平台的渠道包请通过平台导出生成。下方的优化参数只影响本地预览,平台导出不会读取它们。
|
|
307
|
+
|
|
314
308
|
### 构建优化
|
|
315
309
|
|
|
316
310
|
```bash
|
|
317
311
|
# 🎬 场景选择(推荐!可减小 30-60% 文件大小)
|
|
318
312
|
# 交互式选择(自动检测多个场景并提示)
|
|
319
|
-
playcraft build
|
|
313
|
+
playcraft build
|
|
320
314
|
|
|
321
315
|
# 指定特定场景
|
|
322
|
-
playcraft build --
|
|
316
|
+
playcraft build --scenes BallGame
|
|
323
317
|
|
|
324
318
|
# 指定多个场景
|
|
325
|
-
playcraft build --
|
|
319
|
+
playcraft build --scenes "MainMenu,Gameplay,Settings"
|
|
326
320
|
|
|
327
321
|
# 启用图片压缩
|
|
328
|
-
playcraft build --
|
|
322
|
+
playcraft build --compress-images --image-quality 75
|
|
329
323
|
|
|
330
324
|
# 启用模型压缩
|
|
331
|
-
playcraft build --
|
|
325
|
+
playcraft build --compress-models --model-compression draco
|
|
332
326
|
|
|
333
327
|
# 生成分析报告
|
|
334
|
-
playcraft build --
|
|
328
|
+
playcraft build --analyze
|
|
335
329
|
|
|
336
330
|
# 组合使用(场景选择 + 图片压缩 + 分析)
|
|
337
|
-
playcraft build --
|
|
331
|
+
playcraft build --scenes MainScene --compress-images --analyze
|
|
338
332
|
```
|
|
339
333
|
|
|
340
334
|
## ⚙️ 配置
|
|
@@ -350,9 +344,6 @@ playcraft build --platform facebook --scenes MainScene --compress-images --analy
|
|
|
350
344
|
"dir": "./"
|
|
351
345
|
},
|
|
352
346
|
"build": {
|
|
353
|
-
"platform": "facebook",
|
|
354
|
-
"format": "html",
|
|
355
|
-
"outputDir": "./dist",
|
|
356
347
|
"useVite": true,
|
|
357
348
|
"compressImages": true,
|
|
358
349
|
"imageQuality": 75,
|
|
@@ -370,6 +361,8 @@ playcraft build --platform facebook --scenes MainScene --compress-images --analy
|
|
|
370
361
|
3. 配置文件(`playcraft.config.json`)
|
|
371
362
|
4. 默认值
|
|
372
363
|
|
|
364
|
+
`build` 字段只为本地构建补充命令行没有给出的优化参数。`playcraft build` 的渠道、格式和输出位置是固定的,配置文件里的 `platform`、`format`、`outputDir` 对它不起作用;渠道包请通过平台导出生成,平台导出不读取这个文件。
|
|
365
|
+
|
|
373
366
|
## 📁 数据目录
|
|
374
367
|
|
|
375
368
|
CLI 会在 `~/.playcraft/` 目录下存储:
|
|
@@ -384,7 +377,7 @@ CLI 会在 `~/.playcraft/` 目录下存储:
|
|
|
384
377
|
当项目包含多个场景时,CLI 会自动提供交互式场景选择:
|
|
385
378
|
|
|
386
379
|
```bash
|
|
387
|
-
playcraft build
|
|
380
|
+
playcraft build
|
|
388
381
|
```
|
|
389
382
|
|
|
390
383
|
**交互界面**:
|
|
@@ -411,10 +404,10 @@ playcraft build --platform facebook
|
|
|
411
404
|
|
|
412
405
|
```bash
|
|
413
406
|
# 指定单个场景
|
|
414
|
-
playcraft build --
|
|
407
|
+
playcraft build --scenes MainScene
|
|
415
408
|
|
|
416
409
|
# 指定多个场景
|
|
417
|
-
playcraft build --
|
|
410
|
+
playcraft build --scenes "MainScene,Gameplay"
|
|
418
411
|
```
|
|
419
412
|
|
|
420
413
|
详见:[场景选择功能文档](../../docs/cli/scene-selection-feature.md)
|
package/dist/commands/build.js
CHANGED
|
@@ -8,9 +8,10 @@ import { cleanupBuildTempDirectory, createBuildTempDirectory } from './build-tem
|
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
|
|
11
|
-
import { isolateWorkspaceForBuild } from '../workspace-runtime/workspaces/
|
|
11
|
+
import { isolateWorkspaceForBuild } from '../workspace-runtime/workspaces/build-isolation.js';
|
|
12
12
|
import { finalizeWorkspacePlayable, readWorkspacePlayable, workspacePlayableSignature } from '../workspace-runtime/previews/finalize.js';
|
|
13
13
|
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
14
|
+
import { WORKSPACE_PLAYABLE_OUTPUT_PATH } from '@playcraft/common/cloud-workspace/runtime';
|
|
14
15
|
/**
|
|
15
16
|
* 类型守卫:检查对象是否符合 PlayableScriptsConfig 类型
|
|
16
17
|
*/
|
|
@@ -247,13 +248,52 @@ async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
|
|
|
247
248
|
suggestedEngine: ammoCheck.suggestedEngine,
|
|
248
249
|
};
|
|
249
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Format and clean the former `playcraft build` flags carried. Vite and draco model compression are
|
|
253
|
+
* already the builder's defaults, and leaving them unset lets playcraft.config.json supply them.
|
|
254
|
+
*/
|
|
255
|
+
const BUILD_DEFAULTS = { format: 'html', clean: true };
|
|
256
|
+
/**
|
|
257
|
+
* `playcraft build`: a local review build of the current project. It always produces one HTML file at
|
|
258
|
+
* `dist/preview/index.html`; channel packaging is done by platform export, not by this command. The
|
|
259
|
+
* tuning options (compression, scenes, engine, ...) apply to this preview only.
|
|
260
|
+
* `output` and `base` are internal: the preview publisher builds into its own scratch folder, and the
|
|
261
|
+
* batch runtime only needs the multi-file base build.
|
|
262
|
+
*/
|
|
263
|
+
export async function previewBuildCommand(projectPath, options = {}) {
|
|
264
|
+
const { output, base, ...tuning } = options;
|
|
265
|
+
const project = path.resolve(projectPath);
|
|
266
|
+
if (base)
|
|
267
|
+
return buildCommand(project, { ...BUILD_DEFAULTS, ...tuning, mode: 'base', ...(output ? { output } : {}) });
|
|
268
|
+
const preview = { ...BUILD_DEFAULTS, ...tuning, mode: 'full', platform: 'playcraft' };
|
|
269
|
+
if (output)
|
|
270
|
+
return buildCommand(project, { ...preview, output });
|
|
271
|
+
const workspace = process.env.PLAYCRAFT_BUILD_ISOLATED !== '1' ? await generatedWorkspace(project) : undefined;
|
|
272
|
+
// A workspace build isolates, validates and publishes the canonical preview itself.
|
|
273
|
+
if (workspace?.root === project)
|
|
274
|
+
return buildCommand(project, preview);
|
|
275
|
+
const scratch = await fs.mkdtemp(path.join(tmpdir(), 'playcraft-preview-build-'));
|
|
276
|
+
try {
|
|
277
|
+
await buildCommand(project, { ...preview, output: scratch });
|
|
278
|
+
const entries = await fs.readdir(scratch, { withFileTypes: true });
|
|
279
|
+
if (entries.length !== 1 || !entries[0].isFile() || !entries[0].name.endsWith('.html'))
|
|
280
|
+
throw new Error('preview_not_single_file');
|
|
281
|
+
const target = path.join(project, WORKSPACE_PLAYABLE_OUTPUT_PATH);
|
|
282
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
283
|
+
await fs.copyFile(path.join(scratch, entries[0].name), target);
|
|
284
|
+
console.log(target);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await fs.rm(scratch, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
250
290
|
export async function buildCommand(projectPath, options) {
|
|
251
291
|
// 解析项目路径 - 在 spinner 之前执行以便调试
|
|
252
292
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
253
293
|
const workspace = options.workspaceArtifact !== false && process.env.PLAYCRAFT_BUILD_ISOLATED !== '1' && options.mode === 'full' && options.platform === 'playcraft'
|
|
254
294
|
&& (!options.format || options.format === 'html') ? await generatedWorkspace(resolvedProjectPath) : undefined;
|
|
255
295
|
const requestedOutput = options.output ? path.resolve(options.output) : path.join(resolvedProjectPath, 'dist');
|
|
256
|
-
const canonicalOutput = [path.join(resolvedProjectPath, 'dist'), path.join(resolvedProjectPath,
|
|
296
|
+
const canonicalOutput = [path.join(resolvedProjectPath, 'dist'), path.join(resolvedProjectPath, path.dirname(WORKSPACE_PLAYABLE_OUTPUT_PATH))].includes(requestedOutput);
|
|
257
297
|
if (workspace?.root === resolvedProjectPath) {
|
|
258
298
|
const prefix = requestedOutput.endsWith(path.sep) ? requestedOutput : `${requestedOutput}${path.sep}`;
|
|
259
299
|
if (requestedOutput === workspace.root || workspace.root.startsWith(prefix))
|
|
@@ -269,7 +309,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
269
309
|
if (entries.length !== 1 || !entries[0].isFile() || !entries[0].name.endsWith('.html'))
|
|
270
310
|
throw new Error('preview_not_single_file');
|
|
271
311
|
await finalizeWorkspacePlayable(workspace.root, await fs.readFile(path.join(output, entries[0].name)), 'playcraft build', { expectedSignature: signature });
|
|
272
|
-
console.log(path.join(workspace.root,
|
|
312
|
+
console.log(path.join(workspace.root, WORKSPACE_PLAYABLE_OUTPUT_PATH));
|
|
273
313
|
try {
|
|
274
314
|
const { bytes } = await readWorkspacePlayable(workspace.root);
|
|
275
315
|
const { previewUrl } = await new AgentApiClient().publishWorkspacePlayable({ workspaceId: workspace.workspaceId, directoryName: path.basename(workspace.root) }, bytes);
|
|
@@ -744,7 +784,32 @@ export async function buildCommand(projectPath, options) {
|
|
|
744
784
|
...(options.tracking === false ? { tracking: { enabled: false } } : {}),
|
|
745
785
|
};
|
|
746
786
|
if (fileConfig) {
|
|
747
|
-
|
|
787
|
+
// playcraft.config.json 只补充调用方没有给出的参数:显式参数,以及上面已确定的渠道、格式和
|
|
788
|
+
// 输出目录始终优先(playcraft build 固定这三项,配置文件不能把它改成渠道包或别的输出位置)。
|
|
789
|
+
const explicit = {
|
|
790
|
+
platform: buildOptions.platform,
|
|
791
|
+
format: buildOptions.format,
|
|
792
|
+
outputDir: buildOptions.outputDir,
|
|
793
|
+
compressEngine: options.compress,
|
|
794
|
+
compressConfigJson: options.compressConfig,
|
|
795
|
+
compressJS: options.compressJs,
|
|
796
|
+
analyze: options.analyze,
|
|
797
|
+
useVite: options.useVite,
|
|
798
|
+
cssMinify: options.cssMinify,
|
|
799
|
+
jsMinify: options.jsMinify,
|
|
800
|
+
compressImages: options.compressImages,
|
|
801
|
+
imageQuality: options.imageQuality,
|
|
802
|
+
convertToWebP: options.convertToWebP,
|
|
803
|
+
compressModels: options.compressModels,
|
|
804
|
+
modelCompression: options.modelCompression,
|
|
805
|
+
esmMode: options.esmMode,
|
|
806
|
+
storeUrls: options.storeUrls,
|
|
807
|
+
tracking: options.tracking === false ? false : undefined,
|
|
808
|
+
};
|
|
809
|
+
for (const [key, value] of Object.entries(fileConfig)) {
|
|
810
|
+
if (explicit[key] === undefined)
|
|
811
|
+
Object.assign(buildOptions, { [key]: value });
|
|
812
|
+
}
|
|
748
813
|
}
|
|
749
814
|
buildOptions.validation = {
|
|
750
815
|
...buildOptions.validation,
|
|
@@ -760,7 +825,7 @@ export async function buildCommand(projectPath, options) {
|
|
|
760
825
|
if (isEditorProject) {
|
|
761
826
|
throw new Error('检测到这是 PlayCanvas 编辑器项目,不是多文件构建产物。\n\n' +
|
|
762
827
|
'请使用以下命令之一:\n' +
|
|
763
|
-
' 1. playcraft build <项目路径> #
|
|
828
|
+
' 1. playcraft build <项目路径> # 本地构建预览(推荐)\n' +
|
|
764
829
|
' 2. playcraft build-base <项目路径> # 先生成多文件产物\n' +
|
|
765
830
|
' playcraft build-playable <产物路径> # 再转换为 Playable\n\n' +
|
|
766
831
|
'💡 提示: build-playable 命令仅用于已有的多文件构建产物');
|
package/dist/commands/create.js
CHANGED
|
@@ -1067,8 +1067,8 @@ function printCreateNextSteps(template, _targetDir, name, here, worktree) {
|
|
|
1067
1067
|
console.log(` ${pc.cyan('playcraft remix init-template --from-atom-tree')}`);
|
|
1068
1068
|
console.log(` ${pc.cyan('npm run verify:contract && npm run dev')}`);
|
|
1069
1069
|
console.log();
|
|
1070
|
-
console.log(pc.dim('
|
|
1071
|
-
console.log(` ${pc.cyan('playcraft build .
|
|
1070
|
+
console.log(pc.dim('本地构建预览(产物在 dist/preview/index.html,渠道包请使用平台导出):'));
|
|
1071
|
+
console.log(` ${pc.cyan('playcraft build .')}`);
|
|
1072
1072
|
console.log();
|
|
1073
1073
|
console.log(pc.dim('正式接入 PlayCraft 后,替换项目 ID:'));
|
|
1074
1074
|
console.log(` ${pc.cyan('playcraft config set --key projectId --value <真实项目ID>')}`);
|
|
@@ -1081,8 +1081,8 @@ function printCreateNextSteps(template, _targetDir, name, here, worktree) {
|
|
|
1081
1081
|
console.log(` ${pc.cyan('npm run build -- google')}`);
|
|
1082
1082
|
console.log(` ${pc.cyan('npm run builds')}`);
|
|
1083
1083
|
console.log();
|
|
1084
|
-
console.log(pc.dim('或使用 PlayCraft CLI
|
|
1085
|
-
console.log(` ${pc.cyan(`playcraft build ${here ? '.' : name}
|
|
1084
|
+
console.log(pc.dim('或使用 PlayCraft CLI 本地构建预览(产物在 dist/preview/index.html):'));
|
|
1085
|
+
console.log(` ${pc.cyan(`playcraft build ${here ? '.' : name}`)}`);
|
|
1086
1086
|
}
|
|
1087
1087
|
console.log();
|
|
1088
1088
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -106,18 +106,16 @@ export async function inspectCommand(projectPath) {
|
|
|
106
106
|
console.log('\n' + pc.bold('💡 建议:\n'));
|
|
107
107
|
if (projectType === 'build') {
|
|
108
108
|
console.log(pc.green('✅ 这是一个构建后的项目,可以直接打包为 Playable Ad'));
|
|
109
|
-
console.log(pc.dim(' 运行: playcraft build
|
|
109
|
+
console.log(pc.dim(' 运行: playcraft build'));
|
|
110
110
|
}
|
|
111
111
|
else if (projectType === 'playcraft-source' || projectType === 'playcanvas-source') {
|
|
112
112
|
console.log(pc.yellow('⚠️ 这是源代码项目,建议先构建再打包\n'));
|
|
113
113
|
console.log(pc.cyan('方式 1 (推荐): 使用 PlayCanvas REST API 构建'));
|
|
114
114
|
console.log(pc.dim(' 1. 在 PlayCanvas Editor 中发布项目'));
|
|
115
115
|
console.log(pc.dim(' 2. 使用 REST API 下载构建'));
|
|
116
|
-
console.log(pc.dim(' 3. 运行: playcraft build
|
|
117
|
-
console.log(pc.cyan('方式 2:
|
|
118
|
-
console.log(pc.dim(' 运行: playcraft build
|
|
119
|
-
console.log(pc.cyan('方式 3: 直接从源代码打包(可能不完整)'));
|
|
120
|
-
console.log(pc.dim(' 运行: playcraft build --platform facebook --skip-build'));
|
|
116
|
+
console.log(pc.dim(' 3. 运行: playcraft build\n'));
|
|
117
|
+
console.log(pc.cyan('方式 2: 直接本地构建'));
|
|
118
|
+
console.log(pc.dim(' 运行: playcraft build'));
|
|
121
119
|
}
|
|
122
120
|
else {
|
|
123
121
|
console.log(pc.red('❌ 无法识别项目类型,请确保:'));
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command, CommanderError } from 'commander';
|
|
2
|
+
import { Command, CommanderError, Option } from 'commander';
|
|
3
3
|
import { accountActivationMessage } from './auth/messages.js';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
@@ -11,7 +11,7 @@ import { stopCommand } from './commands/stop.js';
|
|
|
11
11
|
import { statusCommand } from './commands/status.js';
|
|
12
12
|
import { logsCommand } from './commands/logs.js';
|
|
13
13
|
import { configCommand } from './commands/config.js';
|
|
14
|
-
import { buildCommand } from './commands/build.js';
|
|
14
|
+
import { buildCommand, previewBuildCommand } from './commands/build.js';
|
|
15
15
|
import { buildAllCommand } from './commands/build-all.js';
|
|
16
16
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
17
17
|
import { checkForUpdates } from './utils/version-checker.js';
|
|
@@ -174,28 +174,34 @@ program
|
|
|
174
174
|
token: options.token,
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
|
-
|
|
177
|
+
/**
|
|
178
|
+
* 只保留命令行上显式给出的参数。commander 的默认值与构建器默认值相同,去掉后 playcraft.config.json
|
|
179
|
+
* 才能补充这些参数;否则默认值会被当成显式参数,压过配置文件。
|
|
180
|
+
*/
|
|
181
|
+
function explicitOptions(command) {
|
|
182
|
+
const options = command.opts();
|
|
183
|
+
return Object.fromEntries(Object.keys(options)
|
|
184
|
+
.filter(key => command.getOptionValueSource(key) !== 'default')
|
|
185
|
+
.map(key => [key, options[key]]));
|
|
186
|
+
}
|
|
187
|
+
/** commander 把 --convert-webp / --no-convert-webp 解析成 convertWebp,构建器使用的字段是 convertToWebP。 */
|
|
188
|
+
function withConvertToWebP({ convertWebp, ...options }) {
|
|
189
|
+
return { ...options, convertToWebP: convertWebp };
|
|
190
|
+
}
|
|
191
|
+
// build 命令 - 本地构建当前项目用于 review,产物固定为 dist/preview/index.html;渠道导出走平台导出
|
|
178
192
|
program
|
|
179
193
|
.command('build')
|
|
180
|
-
.description('
|
|
194
|
+
.description('本地构建当前项目,产物输出到 dist/preview/index.html(渠道导出请使用平台导出)')
|
|
181
195
|
.argument('[project-path]', '项目路径', process.cwd())
|
|
182
|
-
.option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo|inmobi|adikteev|remerge|mintegral)')
|
|
183
|
-
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
184
|
-
.option('-o, --output <path>', '输出目录', './dist')
|
|
185
196
|
.option('-c, --config <file>', '配置文件路径')
|
|
186
197
|
.option('-s, --scenes <scenes>', '选择场景: "all"=所有场景, "default"=默认场景, 或逗号分隔的场景名,如 "MainMenu,Gameplay"')
|
|
187
198
|
.option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
|
|
188
|
-
.option('--clean', '构建前清理旧的输出目录(默认启用)', true)
|
|
189
|
-
.option('--no-clean', '跳过清理旧的输出目录')
|
|
190
199
|
.option('--compress', '压缩引擎代码(使用 LZ4,仅 PlayCanvas)')
|
|
191
200
|
.option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积,仅 PlayCanvas)')
|
|
192
201
|
.option('--compress-js', '压缩所有 JS 代码(LZ4 源码保护,浏览器中无法查看变量名和方法名)')
|
|
193
202
|
.option('--analyze', '生成打包分析报告')
|
|
194
203
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon(仅 PlayCanvas)')
|
|
195
204
|
.option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon),仅 PlayCanvas')
|
|
196
|
-
.option('--auto-build', '如果是源代码项目,自动执行本地构建')
|
|
197
|
-
.option('--skip-build', '跳过本地构建,直接从源代码打包(可能不完整)')
|
|
198
|
-
.option('--mode <mode>', '构建模式 (full|base|playable)')
|
|
199
205
|
.option('--use-vite', '使用 Vite 构建(默认启用)', true)
|
|
200
206
|
.option('--no-use-vite', '禁用 Vite,使用旧构建器')
|
|
201
207
|
.option('--css-minify', '压缩 CSS(默认根据平台配置)')
|
|
@@ -212,8 +218,10 @@ program
|
|
|
212
218
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
213
219
|
.option('--use-playable-scripts', '使用 @playcraft/devkit 构建工具打包(可玩广告项目专用)', false)
|
|
214
220
|
.option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
|
|
215
|
-
.
|
|
216
|
-
|
|
221
|
+
.addOption(new Option('-o, --output <path>').hideHelp())
|
|
222
|
+
.addOption(new Option('--base').hideHelp())
|
|
223
|
+
.action(async (projectPath, _options, command) => {
|
|
224
|
+
await previewBuildCommand(projectPath, withConvertToWebP(explicitOptions(command)));
|
|
217
225
|
});
|
|
218
226
|
// build-base 命令 - 只执行阶段1:基础构建
|
|
219
227
|
program
|
|
@@ -262,9 +270,9 @@ program
|
|
|
262
270
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
263
271
|
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
264
272
|
.option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
|
|
265
|
-
.action(async (projectPath,
|
|
273
|
+
.action(async (projectPath, _options, command) => {
|
|
266
274
|
await buildCommand(projectPath, {
|
|
267
|
-
...
|
|
275
|
+
...withConvertToWebP(explicitOptions(command)),
|
|
268
276
|
analyze: true,
|
|
269
277
|
});
|
|
270
278
|
});
|
|
@@ -300,9 +308,9 @@ program
|
|
|
300
308
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
301
309
|
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
302
310
|
.option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
|
|
303
|
-
.action(async (baseBuildDir,
|
|
311
|
+
.action(async (baseBuildDir, _options, command) => {
|
|
304
312
|
await buildCommand(baseBuildDir, {
|
|
305
|
-
...
|
|
313
|
+
...withConvertToWebP(explicitOptions(command)),
|
|
306
314
|
mode: 'playable',
|
|
307
315
|
});
|
|
308
316
|
});
|
|
@@ -343,7 +351,7 @@ program
|
|
|
343
351
|
.option('--theme <theme>', '指定要构建的主题(devkit 项目专用)')
|
|
344
352
|
.option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
|
|
345
353
|
.action(async (projectPath, options) => {
|
|
346
|
-
await buildAllCommand(projectPath, options);
|
|
354
|
+
await buildAllCommand(projectPath, withConvertToWebP(options));
|
|
347
355
|
});
|
|
348
356
|
// 新增:inspect 命令 - 检查项目类型
|
|
349
357
|
program
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import sharp from 'sharp';
|
|
7
|
+
import { canonicalModelAdaptation, createModelAdaptationCore, inspectModelAdaptationSource, readModelSourceDocument, workspaceModelAdaptationSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
let serialized = Promise.resolve();
|
|
11
|
+
function assert(condition, code) { if (!condition)
|
|
12
|
+
throw Error(code); }
|
|
13
|
+
/** Actual engine loading and decoded pixels, with no renderer, browser or project code execution. */
|
|
14
|
+
export function inspectRigidModel(input) {
|
|
15
|
+
const result = serialized.then(() => inspect(input));
|
|
16
|
+
serialized = result.catch(() => undefined);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
async function inspect(input) {
|
|
20
|
+
input.signal?.throwIfAborted();
|
|
21
|
+
const parsed = readModelSourceDocument(input.bytes), document = parsed.document;
|
|
22
|
+
const { binary, extraChunks } = parsed;
|
|
23
|
+
assert(binary && !extraChunks?.length && !(document.buffers ?? []).some((value) => value.uri)
|
|
24
|
+
&& !(document.images ?? []).some((value) => value.uri), 'model_prepare_self_contained_glb_required');
|
|
25
|
+
assert(!(document.extensionsRequired ?? []).some((name) => ['KHR_draco_mesh_compression', 'KHR_texture_basisu', 'EXT_meshopt_compression'].includes(name)), 'model_prepare_decoder_not_configured');
|
|
26
|
+
assert((document.images ?? []).length <= 64, 'model_prepare_image_bound');
|
|
27
|
+
const threeEntry = require.resolve('three'), threeRoot = dirname(dirname(threeEntry));
|
|
28
|
+
const threePackage = JSON.parse(await readFile(join(threeRoot, 'package.json'), 'utf8'));
|
|
29
|
+
assert(threePackage.version === '0.183.2', 'model_prepare_loader_version_mismatch');
|
|
30
|
+
// Variable module URLs avoid inventing a second public engine type contract.
|
|
31
|
+
const THREE = await import(pathToFileURL(join(threeRoot, 'build/three.module.js')).href);
|
|
32
|
+
const loaderPath = require.resolve('three/examples/jsm/loaders/GLTFLoader.js');
|
|
33
|
+
const { GLTFLoader } = await import(pathToFileURL(loaderPath).href);
|
|
34
|
+
const descriptors = new Map(['self', 'ProgressEvent', 'createImageBitmap'].map(name => [name, Object.getOwnPropertyDescriptor(globalThis, name)]));
|
|
35
|
+
const pending = new Set(), decoded = [];
|
|
36
|
+
let loaded, factory;
|
|
37
|
+
const errors = [];
|
|
38
|
+
let decodedBytes = 0;
|
|
39
|
+
try {
|
|
40
|
+
Object.defineProperty(globalThis, 'self', { configurable: true, value: globalThis });
|
|
41
|
+
Object.defineProperty(globalThis, 'ProgressEvent', { configurable: true, value: class extends Event {
|
|
42
|
+
lengthComputable;
|
|
43
|
+
loaded;
|
|
44
|
+
total;
|
|
45
|
+
constructor(type, init = {}) { super(type); this.lengthComputable = Boolean(init.lengthComputable); this.loaded = init.loaded ?? 0; this.total = init.total ?? 0; }
|
|
46
|
+
} });
|
|
47
|
+
Object.defineProperty(globalThis, 'createImageBitmap', { configurable: true, value: (blob, options = {}) => {
|
|
48
|
+
const task = (async () => {
|
|
49
|
+
assert(blob instanceof Blob && blob.size <= 32 * 1024 * 1024, 'model_prepare_image_bound');
|
|
50
|
+
const bytes = Buffer.from(await blob.arrayBuffer());
|
|
51
|
+
let decoder = sharp(bytes, { limitInputPixels: 16_777_216 });
|
|
52
|
+
if (options.imageOrientation === 'flipY')
|
|
53
|
+
decoder = decoder.flip();
|
|
54
|
+
const metadata = await decoder.metadata(), allocation = (metadata.width ?? 0) * (metadata.height ?? 0) * 4;
|
|
55
|
+
assert(allocation > 0 && decodedBytes + allocation <= 128 * 1024 * 1024, 'model_prepare_decoded_image_budget');
|
|
56
|
+
decodedBytes += allocation;
|
|
57
|
+
const raw = await decoder.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
58
|
+
assert(raw.info.channels === 4 && raw.data.length === raw.info.width * raw.info.height * 4, 'model_prepare_image_decode_failed');
|
|
59
|
+
const image = { width: raw.info.width, height: raw.info.height, data: new Uint8ClampedArray(raw.data), close() { this.data = null; } };
|
|
60
|
+
decoded.push({ image, encodedHash: hash(bytes), rgbaHash: hash(raw.data), width: image.width, height: image.height });
|
|
61
|
+
return image;
|
|
62
|
+
})();
|
|
63
|
+
pending.add(task);
|
|
64
|
+
void task.then(() => pending.delete(task), () => pending.delete(task));
|
|
65
|
+
return task;
|
|
66
|
+
} });
|
|
67
|
+
const manager = new THREE.LoadingManager();
|
|
68
|
+
manager.onError = () => errors.push('embedded-resource-load-failed');
|
|
69
|
+
loaded = await new GLTFLoader(manager).parseAsync(input.bytes.buffer.slice(input.bytes.byteOffset, input.bytes.byteOffset + input.bytes.byteLength), '');
|
|
70
|
+
await Promise.all([...pending]);
|
|
71
|
+
input.signal?.throwIfAborted();
|
|
72
|
+
assert(!errors.length, 'model_prepare_resource_load_failed');
|
|
73
|
+
const inventory = await inspectModelAdaptationSource({ source: loaded.scene, animationCount: loaded.animations.length, digestBytes: hash, signal: input.signal });
|
|
74
|
+
assert(!inventory.unsupported.length, 'model_prepare_rigid_source_required');
|
|
75
|
+
let triangles = 0;
|
|
76
|
+
loaded.scene.traverse((node) => {
|
|
77
|
+
if (node.isMesh && node.geometry) {
|
|
78
|
+
const count = node.geometry.index?.count ?? node.geometry.getAttribute('position')?.count ?? 0;
|
|
79
|
+
assert(Number.isSafeInteger(count) && count % 3 === 0, 'model_prepare_triangles_required');
|
|
80
|
+
triangles += count / 3;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
assert(triangles > 0, 'model_prepare_empty_model');
|
|
84
|
+
let plan = workspaceModelAdaptationSchema.parse(input.adaptation ? { ...input.adaptation, source: input.source } : { schemaVersion: 1, source: input.source });
|
|
85
|
+
if (input.split) {
|
|
86
|
+
const ref = (role) => {
|
|
87
|
+
const expected = input.split.output.nodes.filter(node => node.role === role);
|
|
88
|
+
assert(expected.length === 1, 'model_prepare_split_roles_invalid');
|
|
89
|
+
const matches = inventory.nodes.filter(node => node.sourceNodeId === expected[0].sourceNodeId && node.name === expected[0].name);
|
|
90
|
+
assert(matches.length === 1 && matches[0].geometryHash, 'model_prepare_split_node_identity_mismatch');
|
|
91
|
+
return { sourceNodeId: matches[0].sourceNodeId, geometryHash: matches[0].geometryHash };
|
|
92
|
+
};
|
|
93
|
+
plan = workspaceModelAdaptationSchema.parse({ ...plan, hinges: [{ id: 'primary', movingNodeRef: ref('lid'), bodyNodeRefs: [ref('body')],
|
|
94
|
+
pivot: input.split.hinge.pivot, axis: input.split.hinge.axis, openAngleDegrees: input.split.hinge.openAngleDegrees }] });
|
|
95
|
+
}
|
|
96
|
+
assert(input.requiredCapability !== 'rigid-hinge' || plan.hinges.length > 0, 'model_prepare_needs_part_mapping');
|
|
97
|
+
const points = (node) => {
|
|
98
|
+
const result = [];
|
|
99
|
+
node.updateMatrixWorld(true);
|
|
100
|
+
node.traverse((item) => {
|
|
101
|
+
const position = item.isMesh && item.geometry?.getAttribute('position');
|
|
102
|
+
if (position)
|
|
103
|
+
for (let index = 0; index < position.count; index++) {
|
|
104
|
+
if (result.length >= 4_000_000)
|
|
105
|
+
throw Error('model_prepare_geometry_bound');
|
|
106
|
+
result.push(new THREE.Vector3().fromBufferAttribute(position, index).applyMatrix4(item.matrixWorld).toArray());
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
const distance = (before, after) => {
|
|
112
|
+
assert(before.length === after.length, 'model_prepare_geometry_changed');
|
|
113
|
+
return before.reduce((maximum, point, index) => Math.max(maximum, Math.hypot(...point.map((value, axis) => value - after[index][axis]))), 0);
|
|
114
|
+
};
|
|
115
|
+
const original = points(loaded.scene);
|
|
116
|
+
factory = await createModelAdaptationCore({ THREE, source: loaded.scene, sourceIdentity: input.source, plan,
|
|
117
|
+
animationCount: loaded.animations.length, digestBytes: hash, signal: input.signal });
|
|
118
|
+
const instance = factory.create(), samples = [];
|
|
119
|
+
try {
|
|
120
|
+
for (const declared of plan.hinges) {
|
|
121
|
+
const hinge = instance.getHinge(declared.id);
|
|
122
|
+
const body = hinge.body.map(node => points(node)), moving = points(hinge.moving);
|
|
123
|
+
for (const fraction of [.25, .5, .75, 1]) {
|
|
124
|
+
input.signal?.throwIfAborted();
|
|
125
|
+
const angleDegrees = declared.openAngleDegrees * fraction;
|
|
126
|
+
instance.setHinge(declared.id, angleDegrees);
|
|
127
|
+
const bodyDisplacement = Math.max(...hinge.body.map((node, index) => distance(body[index], points(node))));
|
|
128
|
+
const movingDisplacement = distance(moving, points(hinge.moving));
|
|
129
|
+
assert(bodyDisplacement <= 1e-8 && movingDisplacement > 1e-8, 'model_prepare_hinge_motion_failed');
|
|
130
|
+
samples.push({ hingeId: declared.id, angleDegrees, bodyDisplacement, movingDisplacement });
|
|
131
|
+
}
|
|
132
|
+
instance.setHinge(declared.id, 0);
|
|
133
|
+
assert(distance(moving, points(hinge.moving)) <= 1e-8, 'model_prepare_closed_recovery_failed');
|
|
134
|
+
}
|
|
135
|
+
assert(distance(original, points(loaded.scene)) === 0, 'model_prepare_source_mutated');
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
instance.dispose();
|
|
139
|
+
}
|
|
140
|
+
const dependencies = await Promise.all([['three', join(threeRoot, 'build/three.module.js')], ['three-core', join(threeRoot, 'build/three.core.js')], ['loader', loaderPath]]
|
|
141
|
+
.map(async ([name, path]) => ({ name: name, contentHash: hash(await readFile(path)) })));
|
|
142
|
+
return { schemaVersion: 1, kind: 'actual-engine-geometry-inspection', source: input.source, plan, inventory, samples, triangles,
|
|
143
|
+
decodedTextures: decoded.map(({ image: _image, ...value }) => value),
|
|
144
|
+
tool: { threeVersion: threePackage.version, sharpVersions: sharp.versions, nodeVersion: process.versions.node, dependencies,
|
|
145
|
+
dependencyHash: hash(canonicalModelAdaptation(dependencies)) },
|
|
146
|
+
rendered: false, browserStarted: false, modelAcceptance: false };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await Promise.allSettled([...pending]);
|
|
150
|
+
factory?.release();
|
|
151
|
+
const disposed = new Set();
|
|
152
|
+
loaded?.scene?.traverse((node) => {
|
|
153
|
+
for (const item of [node.geometry, ...(Array.isArray(node.material) ? node.material : node.material ? [node.material] : [])]) {
|
|
154
|
+
if (!item || disposed.has(item))
|
|
155
|
+
continue;
|
|
156
|
+
disposed.add(item);
|
|
157
|
+
for (const value of Object.values(item))
|
|
158
|
+
if (value?.isTexture && !disposed.has(value)) {
|
|
159
|
+
disposed.add(value);
|
|
160
|
+
value.dispose();
|
|
161
|
+
}
|
|
162
|
+
item.dispose();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
for (const entry of decoded)
|
|
166
|
+
entry.image.close();
|
|
167
|
+
for (const [name, descriptor] of descriptors)
|
|
168
|
+
if (descriptor)
|
|
169
|
+
Object.defineProperty(globalThis, name, descriptor);
|
|
170
|
+
else
|
|
171
|
+
delete globalThis[name];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -120,7 +120,7 @@ try {
|
|
|
120
120
|
export const modelProjectStyle = `body{margin:0;background:#eee9e0;color:#202d38;font:16px system-ui}main{max-width:1120px;margin:auto;padding:24px}#stage{height:62vh;min-height:240px;border:1px solid #c2ccd1;border-radius:8px;overflow:hidden}canvas{display:block;width:100%;height:100%}#controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px}button,input{padding:10px 18px;border:1px solid currentColor;border-radius:4px;background:#f8f4ec;color:inherit}button{cursor:pointer}label{display:inline-flex;align-items:center;gap:8px}input{width:110px}
|
|
121
121
|
`;
|
|
122
122
|
export const modelProjectPrepare = String.raw `import { createHash } from 'node:crypto';
|
|
123
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
123
|
+
import { readFile, readdir, lstat } from 'node:fs/promises';
|
|
124
124
|
import { join, relative } from 'node:path';
|
|
125
125
|
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
126
126
|
const canonical = value => Array.isArray(value) ? '[' + value.map(canonical).join(',') + ']' : value && typeof value === 'object'
|
|
@@ -137,13 +137,45 @@ export async function prepareModelBuild(root) {
|
|
|
137
137
|
}
|
|
138
138
|
const packed = await readFile(join(root, 'src/assets/packed-model.glb')); if (hash(packed) !== meta.packedHash) throw Error('model_packed_bytes_changed');
|
|
139
139
|
const core = await readFile(join(root, 'src/runtime/model-adaptation-core.js')); if (hash(core) !== meta.coreHash) throw Error('model_core_changed');
|
|
140
|
-
const inputs = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
const inputs = [];
|
|
141
|
+
if (meta.buildBindingVersion === 2) {
|
|
142
|
+
const declaration = JSON.parse(await readFile(join(root, '.playcraft/model-build-inputs.json'), 'utf8'));
|
|
143
|
+
const model = declaration.models?.[0];
|
|
144
|
+
if (declaration.schemaVersion !== 1 || !Array.isArray(declaration.models) || declaration.models.length !== 1 ||
|
|
145
|
+
model?.source?.kind !== 'resource' || canonical(model.source) !== canonical(plan.source) || !Array.isArray(model.adaptationFiles)) throw Error('model_build_manifest_invalid');
|
|
146
|
+
const paths = new Map();
|
|
147
|
+
for (const file of [...model.source.files, ...model.adaptationFiles]) {
|
|
148
|
+
if (!file || typeof file.path !== 'string' || !file.path || file.path.length > 512 || /[\\\0:]/.test(file.path) || file.path.startsWith('/') ||
|
|
149
|
+
file.path.split('/').some(part => !part || part === '.' || part === '..') ||
|
|
150
|
+
file.contentHash !== undefined && !/^[a-f0-9]{64}$/.test(file.contentHash)) throw Error('model_build_manifest_invalid');
|
|
151
|
+
const previous = paths.get(file.path); if (previous?.contentHash && file.contentHash && previous.contentHash !== file.contentHash) throw Error('model_build_input_conflict');
|
|
152
|
+
paths.set(file.path, previous?.contentHash ? previous : file);
|
|
153
|
+
}
|
|
154
|
+
if (paths.size > 256) throw Error('model_build_input_limit');
|
|
155
|
+
const readInput = async path => {
|
|
156
|
+
let current = root; for (const [index, part] of path.split('/').entries()) {
|
|
157
|
+
current = join(current, part); const stat = await lstat(current);
|
|
158
|
+
if (stat.isSymbolicLink() || (index < path.split('/').length - 1 ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > 64 * 1024 * 1024)) throw Error('model_input_not_regular');
|
|
159
|
+
}
|
|
160
|
+
return readFile(current);
|
|
161
|
+
};
|
|
162
|
+
for (const path of ['package.json','pnpm-lock.yaml','package-lock.json','yarn.lock']) {
|
|
163
|
+
try { const bytes = await readInput(path); if (!paths.has(path)) inputs.push({ path, contentHash: hash(bytes) }); }
|
|
164
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
165
|
+
}
|
|
166
|
+
for (const file of paths.values()) {
|
|
167
|
+
const contentHash = hash(await readInput(file.path)); if (file.contentHash && contentHash !== file.contentHash) throw Error('model_build_input_changed');
|
|
168
|
+
inputs.push({ path: file.path, contentHash });
|
|
169
|
+
}
|
|
170
|
+
} else if (meta.buildBindingVersion === undefined || meta.buildBindingVersion === 1) {
|
|
171
|
+
async function walk(directory) { for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
172
|
+
const path = join(directory, entry.name); if (entry.isDirectory()) await walk(path); else if (entry.isFile()) inputs.push({ path: relative(root, path).split('\\').join('/'), contentHash: hash(await readFile(path)) }); else throw Error('model_input_not_regular');
|
|
173
|
+
} }
|
|
174
|
+
await walk(join(root, 'src')); await walk(join(root, 'source')); await walk(join(root, 'scripts'));
|
|
175
|
+
for (const path of ['index.html','package.json','vite.config.mjs','.playcraft/workspace.json','.playcraft/model-adaptation.json','.playcraft/model-adaptation-lineage.json','package-lock.json','pnpm-lock.yaml']) {
|
|
176
|
+
try { inputs.push({ path, contentHash: hash(await readFile(join(root, path))) }); } catch (error) { if (error.code !== 'ENOENT' || !path.endsWith('lock.json') && path !== 'pnpm-lock.yaml') throw error; }
|
|
177
|
+
}
|
|
178
|
+
} else throw Error('model_build_binding_version_unsupported');
|
|
147
179
|
inputs.sort((a,b) => a.path.localeCompare(b.path, 'en'));
|
|
148
180
|
const binding = { sourceKind: 'external-model', candidateId: meta.candidateId, sourceHash: plan.source.contentHash,
|
|
149
181
|
adaptationHash: hash(await readFile(join(root, 'src/adapter.js'))), buildHash: hash(canonical(inputs)) };
|