@htyf-mp/cli 0.2.0-alpha.7 → 0.2.0-alpha.8
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/package.json +1 -1
- package/src/build.mjs +47 -25
- package/src/export_godot.mjs +219 -0
- package/src/index.mjs +6 -3
- package/src/utils.mjs +25 -2
package/package.json
CHANGED
package/src/build.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import fse from 'fs-extra';
|
|
4
3
|
import AdmZip from 'adm-zip';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Logger } from './logger.mjs';
|
|
6
6
|
import { CONSTANTS } from './constants.mjs';
|
|
7
7
|
import { FileSystemUtils } from './file-system.mjs';
|
|
8
8
|
import { CommandExecutor } from './command-executor.mjs';
|
|
9
9
|
import { getMiniAppScriptId } from './utils-functions.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { getProjectRoot, guessGodotProjectDir } from './utils.mjs';
|
|
11
|
+
import { exportGodot, promptGodotOptions } from './export_godot.mjs';
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
15
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* 压缩文件夹为zip文件
|
|
@@ -22,16 +24,16 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
22
24
|
Logger.info(`开始压缩文件夹: ${inputPath}`);
|
|
23
25
|
Logger.info(`输出文件: ${outputPath}`);
|
|
24
26
|
|
|
25
|
-
if (!
|
|
27
|
+
if (!(await fse.pathExists(inputPath))) {
|
|
26
28
|
throw new Error(`输入路径不存在: ${inputPath}`);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const stats =
|
|
31
|
+
const stats = await fse.stat(inputPath);
|
|
30
32
|
if (!stats.isDirectory()) {
|
|
31
33
|
throw new Error(`输入路径不是目录: ${inputPath}`);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
const files =
|
|
36
|
+
const files = await fse.readdir(inputPath);
|
|
35
37
|
Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
|
|
36
38
|
|
|
37
39
|
if (files.length === 0) {
|
|
@@ -56,7 +58,7 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
56
58
|
await admzip.writeZipPromise(zipPath);
|
|
57
59
|
Logger.success(`压缩完成: ${zipPath}`);
|
|
58
60
|
|
|
59
|
-
const zipStats =
|
|
61
|
+
const zipStats = await fse.stat(zipPath);
|
|
60
62
|
Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
|
|
61
63
|
|
|
62
64
|
return zipPath;
|
|
@@ -71,7 +73,7 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
71
73
|
* @param {object} newAppInfo - 应用信息
|
|
72
74
|
* @returns {Promise<string>} 压缩包路径
|
|
73
75
|
*/
|
|
74
|
-
export async function mpBuildShell(newAppInfo) {
|
|
76
|
+
export async function mpBuildShell(newAppInfo, isGodot) {
|
|
75
77
|
Logger.info(`开始构建小程序包...`);
|
|
76
78
|
Logger.info(`应用: ${newAppInfo.name}`);
|
|
77
79
|
Logger.info(`应用ID: ${newAppInfo.appid}`);
|
|
@@ -94,22 +96,22 @@ export async function mpBuildShell(newAppInfo) {
|
|
|
94
96
|
throw new Error('无法创建输出目录');
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
const version = newAppInfo
|
|
98
|
-
const appid = newAppInfo.appid;
|
|
99
|
+
const { version, appid } = newAppInfo;
|
|
99
100
|
const scriptName = getMiniAppScriptId(appid, version);
|
|
100
101
|
|
|
101
102
|
const appJson = {
|
|
102
103
|
type: 'app',
|
|
103
104
|
engines: '2.0.0',
|
|
104
105
|
name: newAppInfo.name,
|
|
105
|
-
appid
|
|
106
|
-
version
|
|
106
|
+
appid,
|
|
107
|
+
version,
|
|
107
108
|
appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
|
|
108
109
|
zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
|
|
109
110
|
};
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
const
|
|
112
|
+
let outputPath = path.join(mpOutputPath, 'dist');
|
|
113
|
+
const distPackagePath = path.join(mpOutputPath, 'dist.dgz');
|
|
114
|
+
const rootIndexPath = path.join(tempPath, '..', 'index.js');
|
|
113
115
|
|
|
114
116
|
const mpOptions = {
|
|
115
117
|
name: scriptName,
|
|
@@ -117,26 +119,46 @@ export async function mpBuildShell(newAppInfo) {
|
|
|
117
119
|
exposes: {
|
|
118
120
|
'App': newAppInfo.entry,
|
|
119
121
|
},
|
|
120
|
-
outputPath: path.join(mpOutputPath, '
|
|
122
|
+
outputPath: path.join(mpOutputPath, 'build'),
|
|
121
123
|
extraChunksPath: outputPath,
|
|
122
|
-
manifest: path.join(mpOutputPath, '
|
|
124
|
+
manifest: path.join(mpOutputPath, 'build'),
|
|
123
125
|
};
|
|
124
126
|
|
|
125
127
|
const appJsonPath = path.join(mpOutputPath, 'app.json');
|
|
126
|
-
|
|
128
|
+
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
127
129
|
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
128
130
|
|
|
129
|
-
const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} webpack --config ${webpackConfigPath}`;
|
|
130
131
|
|
|
131
132
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
if (!isGodot) {
|
|
134
|
+
const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} webpack --config ${webpackConfigPath}`;
|
|
135
|
+
await CommandExecutor.execute(command, {
|
|
136
|
+
description: '构建小程序包',
|
|
137
|
+
timeout: 120000,
|
|
138
|
+
showCommand: false
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
Logger.info('检测到 Godot 项目,开始执行 Godot 导出流程...');
|
|
142
|
+
const defaultPlatform = ['android', 'ios'].includes(CONSTANTS.BUNDLE_PLATFORM)
|
|
143
|
+
? CONSTANTS.BUNDLE_PLATFORM
|
|
144
|
+
: 'ios';
|
|
145
|
+
const godotDefaults = {
|
|
146
|
+
targetBaseDir: outputPath,
|
|
147
|
+
projectDir: guessGodotProjectDir(PROJECT_ROOT),
|
|
148
|
+
name: newAppInfo.name || 'GodotApp',
|
|
149
|
+
preset: defaultPlatform === 'android' ? 'Android' : 'iOS',
|
|
150
|
+
platform: defaultPlatform
|
|
151
|
+
};
|
|
152
|
+
const godotOptions = await promptGodotOptions(godotDefaults);
|
|
153
|
+
outputPath = path.resolve(godotOptions.targetBaseDir);
|
|
154
|
+
await fse.ensureDir(outputPath);
|
|
155
|
+
await fse.emptyDir(outputPath);
|
|
156
|
+
await exportGodot(godotOptions);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await fse.ensureDir(outputPath);
|
|
160
|
+
await fse.copy(appJsonPath, path.join(outputPath, 'app.json'));
|
|
161
|
+
const zipPath = await handleZip(outputPath, distPackagePath);
|
|
140
162
|
Logger.success(`压缩包已创建: ${zipPath}`);
|
|
141
163
|
return zipPath;
|
|
142
164
|
} catch (error) {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { accessSync, constants as fsConstants } from 'node:fs';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import AdmZip from 'adm-zip';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_GODOT_BIN = '/Applications/Godot.app/Contents/MacOS/Godot';
|
|
14
|
+
const GODOT_CACHE_DIR = path.join(homedir() || process.cwd(), '.htyf');
|
|
15
|
+
const GODOT_CACHE_FILE = path.join(GODOT_CACHE_DIR, 'godot-config.json');
|
|
16
|
+
|
|
17
|
+
const PLATFORMS = {
|
|
18
|
+
IOS: 'ios',
|
|
19
|
+
ANDROID: 'android'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PRESET_NAMES = {
|
|
23
|
+
[PLATFORMS.IOS]: 'iOS',
|
|
24
|
+
[PLATFORMS.ANDROID]: 'Android'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function promptGodotOptions(defaults = {}) {
|
|
28
|
+
const answers = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'targetBaseDir',
|
|
32
|
+
message: '请输入导出目标根目录:',
|
|
33
|
+
default: defaults.targetBaseDir || path.resolve(process.cwd(), './dist'),
|
|
34
|
+
validate: (input) => !!input.trim() || '目标根目录不能为空'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'projectDir',
|
|
39
|
+
message: '请输入 Godot 项目目录:',
|
|
40
|
+
default: defaults.projectDir || process.cwd(),
|
|
41
|
+
validate: (input) => !!input.trim() || '项目目录不能为空'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'input',
|
|
45
|
+
name: 'name',
|
|
46
|
+
message: '请输入导出文件名称 (不含扩展名):',
|
|
47
|
+
default: defaults.name || 'GodotExport',
|
|
48
|
+
validate: (input) => !!input.trim() || '导出文件名称不能为空'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'input',
|
|
52
|
+
name: 'preset',
|
|
53
|
+
message: '请输入 Godot 导出预设名称:',
|
|
54
|
+
default: defaults.preset || '',
|
|
55
|
+
validate: (input) => !!input.trim() || '导出预设名称不能为空'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'platform',
|
|
60
|
+
message: '请选择导出平台:',
|
|
61
|
+
choices: [
|
|
62
|
+
{ name: 'iOS', value: PLATFORMS.IOS },
|
|
63
|
+
{ name: 'Android', value: PLATFORMS.ANDROID }
|
|
64
|
+
],
|
|
65
|
+
default: defaults.platform || 'ios'
|
|
66
|
+
}
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
targetBaseDir: answers.targetBaseDir.trim(),
|
|
71
|
+
projectDir: answers.projectDir.trim(),
|
|
72
|
+
name: answers.name.trim(),
|
|
73
|
+
preset: answers.preset.trim(),
|
|
74
|
+
platform: answers.platform
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isExecutable(filePath) {
|
|
79
|
+
if (!filePath) return false;
|
|
80
|
+
if (!fs.existsSync(filePath)) return false;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
accessSync(filePath, fsConstants.X_OK);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readCachedGodotPath() {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(GODOT_CACHE_FILE)) {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
const data = fs.readJsonSync(GODOT_CACHE_FILE);
|
|
96
|
+
return typeof data?.godotPath === 'string' ? data.godotPath : '';
|
|
97
|
+
} catch {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function writeCachedGodotPath(godotPath) {
|
|
103
|
+
try {
|
|
104
|
+
await fs.ensureDir(GODOT_CACHE_DIR);
|
|
105
|
+
await fs.writeJson(GODOT_CACHE_FILE, { godotPath }, { spaces: 2 });
|
|
106
|
+
} catch {
|
|
107
|
+
// cache failures should not block execution
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function promptForGodotBinaryPath(defaultPath) {
|
|
112
|
+
const { godotPath } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'godotPath',
|
|
116
|
+
message: '请输入 Godot Editor 可执行文件路径:',
|
|
117
|
+
default: defaultPath,
|
|
118
|
+
validate: (input) => !!input.trim() || '路径不能为空'
|
|
119
|
+
}
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
return godotPath.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveGodotBinary() {
|
|
126
|
+
const override = process.env.GODOT_EDITOR ? process.env.GODOT_EDITOR.trim() : '';
|
|
127
|
+
if (override && isExecutable(override)) {
|
|
128
|
+
return override;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cached = readCachedGodotPath();
|
|
132
|
+
if (cached && isExecutable(cached)) {
|
|
133
|
+
return cached;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isExecutable(DEFAULT_GODOT_BIN)) {
|
|
137
|
+
await writeCachedGodotPath(DEFAULT_GODOT_BIN);
|
|
138
|
+
return DEFAULT_GODOT_BIN;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fallbackDefault = override || cached || DEFAULT_GODOT_BIN;
|
|
142
|
+
const godotPath = await promptForGodotBinaryPath(fallbackDefault);
|
|
143
|
+
|
|
144
|
+
if (!isExecutable(godotPath)) {
|
|
145
|
+
throw new Error(`Godot 二进制文件不可执行: ${godotPath}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await writeCachedGodotPath(godotPath);
|
|
149
|
+
return godotPath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runGodotCommand(godotBin, args) {
|
|
153
|
+
await execa(godotBin, args, { stdio: 'inherit' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function handleAndroidExport({ godotBin, projectDir, preset, targetBaseDir, name }) {
|
|
157
|
+
const assetsBaseDir = path.join(targetBaseDir, 'android', 'app', 'src', 'main', 'assets');
|
|
158
|
+
await fs.ensureDir(assetsBaseDir);
|
|
159
|
+
|
|
160
|
+
const zipPath = path.join(assetsBaseDir, `${name}.zip`);
|
|
161
|
+
await runGodotCommand(godotBin, ['--headless', '--path', projectDir, '--export-pack', preset, zipPath]);
|
|
162
|
+
|
|
163
|
+
const targetDir = path.join(assetsBaseDir, name);
|
|
164
|
+
await fs.remove(targetDir);
|
|
165
|
+
await fs.ensureDir(targetDir);
|
|
166
|
+
|
|
167
|
+
const zip = new AdmZip(zipPath);
|
|
168
|
+
zip.extractAllTo(targetDir, true);
|
|
169
|
+
await fs.remove(zipPath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function handleIosExport({ godotBin, projectDir, preset, targetBaseDir, name }) {
|
|
173
|
+
const assetsBaseDir = path.join(targetBaseDir, 'ios');
|
|
174
|
+
await fs.ensureDir(assetsBaseDir);
|
|
175
|
+
const outputPath = path.join(assetsBaseDir, `${name}.pck`);
|
|
176
|
+
await runGodotCommand(godotBin, ['--headless', '--path', projectDir, '--export-pack', preset, outputPath]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function exportGodot(options) {
|
|
180
|
+
const godotBin = await resolveGodotBinary();
|
|
181
|
+
const { platform, projectDir, preset, targetBaseDir, name } = options;
|
|
182
|
+
|
|
183
|
+
// 执行两次 import 以确保资源正确导入
|
|
184
|
+
const importArgs = ['--headless', '--path', projectDir, '--import'];
|
|
185
|
+
await runGodotCommand(godotBin, importArgs);
|
|
186
|
+
await runGodotCommand(godotBin, importArgs);
|
|
187
|
+
|
|
188
|
+
switch (platform) {
|
|
189
|
+
case PLATFORMS.IOS:
|
|
190
|
+
await handleIosExport({ godotBin, projectDir, preset, targetBaseDir, name });
|
|
191
|
+
break;
|
|
192
|
+
case PLATFORMS.ANDROID:
|
|
193
|
+
await handleAndroidExport({ godotBin, projectDir, preset, targetBaseDir, name });
|
|
194
|
+
break;
|
|
195
|
+
default:
|
|
196
|
+
throw new Error(`Unsupported platform: ${platform}. Supported: ${Object.values(PLATFORMS).join(', ')}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function main() {
|
|
201
|
+
const options = await promptGodotOptions();
|
|
202
|
+
await exportGodot(options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const isCliExecution = (() => {
|
|
206
|
+
if (!process.argv[1]) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const cliUrl = pathToFileURL(process.argv[1]).href;
|
|
210
|
+
return cliUrl === import.meta.url;
|
|
211
|
+
})();
|
|
212
|
+
|
|
213
|
+
if (isCliExecution) {
|
|
214
|
+
main().catch((error) => {
|
|
215
|
+
console.error(error.message || error);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
package/src/index.mjs
CHANGED
|
@@ -136,7 +136,10 @@ async function Start(action) {
|
|
|
136
136
|
|
|
137
137
|
// 显示应用选择界面
|
|
138
138
|
try {
|
|
139
|
-
const
|
|
139
|
+
const appConfigPath = path.join(projectPath, 'app.json');
|
|
140
|
+
const appInfo = fse.readJsonSync(appConfigPath).htyf;
|
|
141
|
+
const isGodot = fse.existsSync(path.join(projectPath, 'game.godot'));
|
|
142
|
+
|
|
140
143
|
if (!appInfo) {
|
|
141
144
|
Logger.error('应用配置不存在,请先在app.json中配置htyf');
|
|
142
145
|
return;
|
|
@@ -185,10 +188,10 @@ async function Start(action) {
|
|
|
185
188
|
|
|
186
189
|
switch (action) {
|
|
187
190
|
case ACTION_TYPES.MP_BUILD:
|
|
188
|
-
await mpBuildShell(newAppInfo);
|
|
191
|
+
await mpBuildShell(newAppInfo, isGodot);
|
|
189
192
|
break;
|
|
190
193
|
case ACTION_TYPES.MP_DEBUG:
|
|
191
|
-
await mpDebugShell(newAppInfo);
|
|
194
|
+
await mpDebugShell(newAppInfo, isGodot);
|
|
192
195
|
break;
|
|
193
196
|
default:
|
|
194
197
|
Logger.error(`未知的操作类型: ${action}`);
|
package/src/utils.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import { fileURLToPath } from 'url';
|
|
3
4
|
|
|
4
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -9,6 +10,28 @@ const __dirname = path.dirname(__filename);
|
|
|
9
10
|
* @returns {string} 项目根目录路径
|
|
10
11
|
*/
|
|
11
12
|
export function getProjectRoot() {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
return process.cwd() || __dirname;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 猜测 Godot 项目目录
|
|
18
|
+
* @param {string} rootDir - 根目录
|
|
19
|
+
* @returns {string} Godot 项目目录路径
|
|
20
|
+
*/
|
|
21
|
+
export function guessGodotProjectDir(rootDir = getProjectRoot()) {
|
|
22
|
+
const candidates = [
|
|
23
|
+
rootDir,
|
|
24
|
+
path.join(rootDir, 'project'),
|
|
25
|
+
path.join(rootDir, 'game'),
|
|
26
|
+
path.join(rootDir, 'godot')
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const dir of candidates) {
|
|
30
|
+
const projectFile = path.join(dir, 'project.godot');
|
|
31
|
+
const gameFile = path.join(dir, 'game.godot');
|
|
32
|
+
if (fs.existsSync(projectFile) || fs.existsSync(gameFile)) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rootDir;
|
|
14
37
|
}
|