@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htyf-mp/cli",
3
- "version": "0.2.0-alpha.7",
3
+ "version": "0.2.0-alpha.8",
4
4
  "description": "红糖云服app小程序模板-cli",
5
5
  "type": "module",
6
6
  "bin": {
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 { fileURLToPath } from 'node:url';
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 (!fs.existsSync(inputPath)) {
27
+ if (!(await fse.pathExists(inputPath))) {
26
28
  throw new Error(`输入路径不存在: ${inputPath}`);
27
29
  }
28
30
 
29
- const stats = fs.statSync(inputPath);
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 = fs.readdirSync(inputPath);
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 = fs.statSync(zipPath);
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.version;
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: appid,
106
- version: 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
- const outputPath = path.join(mpOutputPath, './dist');
112
- const rootIndexPath = path.join(tempPath, '../index.js');
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, './build'),
122
+ outputPath: path.join(mpOutputPath, 'build'),
121
123
  extraChunksPath: outputPath,
122
- manifest: path.join(mpOutputPath, './build'),
124
+ manifest: path.join(mpOutputPath, 'build'),
123
125
  };
124
126
 
125
127
  const appJsonPath = path.join(mpOutputPath, 'app.json');
126
- fs.writeFileSync(appJsonPath, JSON.stringify(appJson, undefined, 2));
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
- await CommandExecutor.execute(command, {
133
- description: '构建小程序包',
134
- timeout: 120000,
135
- showCommand: false
136
- });
137
-
138
- fs.copyFileSync(appJsonPath, path.join(outputPath, `./app.json`));
139
- const zipPath = await handleZip(outputPath, path.join(outputPath, `../dist.dgz`));
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 appInfo = fse.readJsonSync(path.join(projectPath, 'app.json')).htyf;
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
- let currentPath = process.cwd() || __dirname;
13
- return currentPath;
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
  }