qdmp-cli 0.1.28 → 0.1.29
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 +6 -1
- package/actions.js +22 -17
- package/api.js +8 -9
- package/constants.js +1 -12
- package/index.js +0 -1
- package/package.json +1 -6
- package/utils/authFlow.js +1 -1
- package/utils/authorized.js +2 -2
- package/utils/common.js +7 -95
- package/utils/gitClone.js +6 -4
- package/utils/projectLoader.js +36 -0
- package/utils/agentLogin.js +0 -106
package/README.md
CHANGED
|
@@ -26,12 +26,17 @@ npm install -g qdmp-cli
|
|
|
26
26
|
{
|
|
27
27
|
"appId": "your_app_id",
|
|
28
28
|
"devAppId": "your_dev_app_id",
|
|
29
|
+
"loader": "EMP"
|
|
29
30
|
}
|
|
30
31
|
```
|
|
31
32
|
### 初始化项目
|
|
32
33
|
```bash
|
|
33
|
-
qdmp-cli create
|
|
34
|
+
qdmp-cli create <app-name> -t default
|
|
34
35
|
```
|
|
36
|
+
新建项目使用官方 2.0(EMP)模板,创建后验证配置。`package.json.version` 是业务包版本,模板中的 `1.0.0` 不代表 1.0 小程序。保留模板的 loader,不通过改写该字段转换工程。
|
|
37
|
+
|
|
38
|
+
关联 AppID 后执行 `qdmp-cli getMe` 核对本地与平台应用类型;上传前也会校验一致性。仅支持 2.0(EMP)项目,不再支持 SPA/effuse 工程;构建必须完成 EMP 编译。
|
|
39
|
+
|
|
35
40
|
### 拉取配置
|
|
36
41
|
```bash
|
|
37
42
|
qdmp-cli init -a appId
|
package/actions.js
CHANGED
|
@@ -7,7 +7,6 @@ import chalk from "chalk";
|
|
|
7
7
|
import table from "table";
|
|
8
8
|
import { inquirerVersionDescription } from "./utils/interactive.js";
|
|
9
9
|
import { ensureInteractiveAuth, interactiveLogin } from "./utils/authFlow.js";
|
|
10
|
-
import { agentLogin } from "./utils/agentLogin.js";
|
|
11
10
|
import { getDeveloperId, getUserInfo } from "./api.js";
|
|
12
11
|
import fs from "fs";
|
|
13
12
|
import path from "path";
|
|
@@ -20,10 +19,11 @@ import {
|
|
|
20
19
|
preUpload,
|
|
21
20
|
cleanWorkSpaceFolder,
|
|
22
21
|
} from "./utils/common.js";
|
|
22
|
+
import { readProjectLoader, assertMatchingLoader, assertEMPTemplate } from "./utils/projectLoader.js";
|
|
23
23
|
import { assertCompilerRuntime } from "./utils/compilerRuntime.js";
|
|
24
24
|
import { getAppInfo, uploadVersion, uploadApp } from "./api.js";
|
|
25
25
|
|
|
26
|
-
function createProject(appName, template) {
|
|
26
|
+
async function createProject(appName, template) {
|
|
27
27
|
printLogo();
|
|
28
28
|
if (template) {
|
|
29
29
|
console.log("\n" + chalk.dim("─".repeat(60)) + "\n");
|
|
@@ -36,7 +36,9 @@ function createProject(appName, template) {
|
|
|
36
36
|
info(` ${chalk.dim("仓库:")} ${chalk.green(template.value)}`);
|
|
37
37
|
info(`⏳ 正在创建 ${chalk.bold(appName)} 项目...`);
|
|
38
38
|
console.log("\n" + chalk.dim("─".repeat(60)) + "\n");
|
|
39
|
-
download(appName, template.value);
|
|
39
|
+
const targetPath = await download(appName, template.value);
|
|
40
|
+
assertEMPTemplate(targetPath);
|
|
41
|
+
success("已验证 2.0(EMP)模板;package.json.version 不代表小程序运行时版本。");
|
|
40
42
|
} else {
|
|
41
43
|
listAction();
|
|
42
44
|
error(`未找到模板`);
|
|
@@ -51,9 +53,13 @@ export const initAction = async (appName, option) => {
|
|
|
51
53
|
if (appName.match(invalidChars)) {
|
|
52
54
|
error("名称不能包含中文及特殊字符");
|
|
53
55
|
}
|
|
54
|
-
const inputTemplate = option.template ||
|
|
56
|
+
const inputTemplate = option.template || "default";
|
|
55
57
|
const template = TEMPLATES.find((t) => t.name === inputTemplate);
|
|
56
|
-
|
|
58
|
+
try {
|
|
59
|
+
await createProject(appName, template);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
error(e.message);
|
|
62
|
+
}
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
export const listAction = () => {
|
|
@@ -80,21 +86,12 @@ export const listAction = () => {
|
|
|
80
86
|
|
|
81
87
|
export const loginAction = async (option) => {
|
|
82
88
|
try {
|
|
83
|
-
if (option.agent) {
|
|
84
|
-
await agentLogin(option.env);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
89
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
88
|
-
throw new Error(
|
|
90
|
+
throw new Error("qdmp login 需要在支持交互输入的终端中执行,或使用 QDMP_TOKEN");
|
|
89
91
|
}
|
|
90
92
|
await interactiveLogin(option.env);
|
|
91
93
|
success("登录成功");
|
|
92
94
|
} catch (e) {
|
|
93
|
-
if (option.agent) {
|
|
94
|
-
console.log(JSON.stringify({ type: "error", env: option.env, message: e.message }));
|
|
95
|
-
process.exitCode = 1;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
95
|
error(e.message);
|
|
99
96
|
} finally {
|
|
100
97
|
// Inquirer may leave stdin in a flowing state after the localhost callback
|
|
@@ -110,6 +107,9 @@ export const getMyInfoAction = async (option) => {
|
|
|
110
107
|
const appInfo = await getAppInfo(appId, option.env);
|
|
111
108
|
info(`当前用户:${nickname}`);
|
|
112
109
|
info(`当前授权应用:${appInfo?.name} - ${appId}`);
|
|
110
|
+
const localLoader = readProjectLoader();
|
|
111
|
+
info(`小程序类型:${appInfo?.loader === "EMP" ? "2.0(EMP)" : "不支持"}`);
|
|
112
|
+
assertMatchingLoader(localLoader, appInfo?.loader);
|
|
113
113
|
} catch (e) {
|
|
114
114
|
error(e?.message);
|
|
115
115
|
}
|
|
@@ -127,6 +127,7 @@ export async function uploadAction(option) {
|
|
|
127
127
|
if (!remoteApp) {
|
|
128
128
|
error("未找到应用,请检查appId是否正确");
|
|
129
129
|
}
|
|
130
|
+
assertMatchingLoader(readProjectLoader(), remoteApp.loader);
|
|
130
131
|
const description = option.description?.trim()
|
|
131
132
|
?? (process.stdin.isTTY ? await inquirerVersionDescription() : "");
|
|
132
133
|
if (Array.from(description).length > 200) {
|
|
@@ -177,9 +178,12 @@ export const initConfigAction = async (option) => {
|
|
|
177
178
|
const appId = option.appId;
|
|
178
179
|
if (!appId) return error("未配置应用ID,请使用-a参数指定应用ID");
|
|
179
180
|
const appInfo = await getAppInfo(appId, option.env);
|
|
181
|
+
if (!appInfo || appInfo.loader !== "EMP") {
|
|
182
|
+
throw new Error("仅支持平台 EMP 应用,未写入配置");
|
|
183
|
+
}
|
|
180
184
|
const config = {
|
|
181
185
|
name: appInfo.name,
|
|
182
|
-
appId: appInfo.appId,
|
|
186
|
+
[option.env === "dev" ? "devAppId" : "appId"]: appInfo.appId,
|
|
183
187
|
loader: appInfo.loader,
|
|
184
188
|
latest: appInfo.latest,
|
|
185
189
|
};
|
|
@@ -197,6 +201,7 @@ export const buildAction = async () => {
|
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
203
|
const config = JSON.parse(fs.readFileSync(qdmpJsonPath, "utf8"));
|
|
204
|
+
readProjectLoader();
|
|
200
205
|
switch (config.loader) {
|
|
201
206
|
case "EMP":
|
|
202
207
|
await assertCompilerRuntime();
|
|
@@ -236,7 +241,7 @@ export const buildAction = async () => {
|
|
|
236
241
|
|
|
237
242
|
default:
|
|
238
243
|
error(
|
|
239
|
-
`不支持的构建类型:${
|
|
244
|
+
`不支持的构建类型:${config.loader ?? "未配置"},请检查 qdmp.json;2.0 必须使用 EMP,不能改用 H5 绕过`,
|
|
240
245
|
);
|
|
241
246
|
break;
|
|
242
247
|
}
|
package/api.js
CHANGED
|
@@ -211,10 +211,10 @@ const unwrapData = (result) => result?.data || result;
|
|
|
211
211
|
const qrcodeLoginURL = (qrcodeId) =>
|
|
212
212
|
`https://qiandao.com/login/scanning/confirm?qrcodeId=${encodeURIComponent(qrcodeId)}`;
|
|
213
213
|
|
|
214
|
-
export const generateLoginQrcode = async (env = "prod"
|
|
214
|
+
export const generateLoginQrcode = async (env = "prod") => {
|
|
215
215
|
const result = await request(
|
|
216
216
|
"/signin/login/scan/qrcode/generate-id",
|
|
217
|
-
{ method: "POST", body: JSON.stringify({})
|
|
217
|
+
{ method: "POST", body: JSON.stringify({}) },
|
|
218
218
|
env,
|
|
219
219
|
false,
|
|
220
220
|
);
|
|
@@ -223,10 +223,10 @@ export const generateLoginQrcode = async (env = "prod", { signal } = {}) => {
|
|
|
223
223
|
return { qrcodeId: String(data.qrcodeId), url: qrcodeLoginURL(data.qrcodeId) };
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
-
export const getLoginQrcodeStatus = async (qrcodeId, env = "prod"
|
|
226
|
+
export const getLoginQrcodeStatus = async (qrcodeId, env = "prod") => {
|
|
227
227
|
const result = await request(
|
|
228
228
|
"/signin/login/scan/qrcode/status",
|
|
229
|
-
{ method: "POST", body: JSON.stringify({ qrcodeId })
|
|
229
|
+
{ method: "POST", body: JSON.stringify({ qrcodeId }) },
|
|
230
230
|
env,
|
|
231
231
|
false,
|
|
232
232
|
);
|
|
@@ -259,17 +259,17 @@ export const loginByPlatformURL = async (env = "prod", onUpdate = () => {}) => {
|
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
-
export const getQiandaoUser = async (token, env = "prod"
|
|
262
|
+
export const getQiandaoUser = async (token, env = "prod") => {
|
|
263
263
|
const result = await request(
|
|
264
264
|
"/user/users/me",
|
|
265
|
-
{ headers: { authorization: `Bearer ${token}` }
|
|
265
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
266
266
|
env,
|
|
267
267
|
false,
|
|
268
268
|
);
|
|
269
269
|
return unwrapData(result);
|
|
270
270
|
};
|
|
271
271
|
|
|
272
|
-
export const exchangeQiandaoLogin = async (token, user, env = "prod"
|
|
272
|
+
export const exchangeQiandaoLogin = async (token, user, env = "prod") => {
|
|
273
273
|
const userInfo = unwrapData(user) || {};
|
|
274
274
|
const thirdPartyUserId = userInfo.id ?? userInfo.userId ?? userInfo.openId;
|
|
275
275
|
if (thirdPartyUserId == null || String(thirdPartyUserId).trim() === "") {
|
|
@@ -279,7 +279,6 @@ export const exchangeQiandaoLogin = async (token, user, env = "prod", { signal,
|
|
|
279
279
|
"/mp/v1/user/login/mobile",
|
|
280
280
|
{
|
|
281
281
|
method: "POST",
|
|
282
|
-
signal,
|
|
283
282
|
body: JSON.stringify({
|
|
284
283
|
thirdPartyUserId: String(thirdPartyUserId),
|
|
285
284
|
nickname: userInfo.nickName || userInfo.nickname || userInfo.name,
|
|
@@ -291,7 +290,7 @@ export const exchangeQiandaoLogin = async (token, user, env = "prod", { signal,
|
|
|
291
290
|
);
|
|
292
291
|
const platformToken = unwrapData(result)?.token;
|
|
293
292
|
if (!platformToken) throw new Error("扫码登录未获取到开放平台 Token");
|
|
294
|
-
|
|
293
|
+
saveToken(platformToken, env);
|
|
295
294
|
return platformToken;
|
|
296
295
|
};
|
|
297
296
|
|
package/constants.js
CHANGED
|
@@ -6,18 +6,7 @@ export const TEMPLATES = [
|
|
|
6
6
|
emoji: "🟢",
|
|
7
7
|
fullUrl: true,
|
|
8
8
|
},
|
|
9
|
-
|
|
10
|
-
// name: "qdmp",
|
|
11
|
-
// value: "frontend/miniapp-taro-template",
|
|
12
|
-
// desc: "千岛小程序默认模版(1.0)",
|
|
13
|
-
// emoji: "🟡",
|
|
14
|
-
// },
|
|
15
|
-
// {
|
|
16
|
-
// name: "h5",
|
|
17
|
-
// value: "frontend/h5-template",
|
|
18
|
-
// desc: "h5模版",
|
|
19
|
-
// emoji: "🔵",
|
|
20
|
-
// },
|
|
9
|
+
|
|
21
10
|
];
|
|
22
11
|
|
|
23
12
|
export const WORKSPACE_DIR = "qdmp-output";
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdmp-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "qdmp-cli",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"is-unicode-supported": "^2.1.0",
|
|
38
38
|
"js-base64": "3.7.8",
|
|
39
39
|
"ora": "^8.2.0",
|
|
40
|
-
"qrcode": "^1.5.4",
|
|
41
40
|
"qrcode-terminal": "^0.12.0",
|
|
42
41
|
"query-string": "7.1.3",
|
|
43
42
|
"shelljs": "^0.10.0",
|
|
@@ -46,9 +45,5 @@
|
|
|
46
45
|
},
|
|
47
46
|
"engines": {
|
|
48
47
|
"node": "^20.19.0 || >=22.12.0"
|
|
49
|
-
},
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"registry": "https://registry.npmjs.org/",
|
|
52
|
-
"access": "public"
|
|
53
48
|
}
|
|
54
49
|
}
|
package/utils/authFlow.js
CHANGED
|
@@ -87,7 +87,7 @@ export const ensureInteractiveAuth = async (env = "prod") => {
|
|
|
87
87
|
} catch (cause) {
|
|
88
88
|
if (!isAuthRequiredError(cause)) throw cause;
|
|
89
89
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
90
|
-
throw new Error(
|
|
90
|
+
throw new Error("当前命令需要登录,请在支持交互输入的终端中重新执行,或设置 QDMP_TOKEN");
|
|
91
91
|
}
|
|
92
92
|
await interactiveLogin(env);
|
|
93
93
|
return ensureAuth(env);
|
package/utils/authorized.js
CHANGED
|
@@ -20,7 +20,7 @@ const readConfig = () => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export const saveToken = (token, env = "prod"
|
|
23
|
+
export const saveToken = (token, env = "prod") => {
|
|
24
24
|
normalizeLoginEnv(env);
|
|
25
25
|
const configPath = getConfigPath();
|
|
26
26
|
const config = readConfig();
|
|
@@ -32,7 +32,7 @@ export const saveToken = (token, env = "prod", { quiet = false } = {}) => {
|
|
|
32
32
|
try {
|
|
33
33
|
fs.writeFileSync(configPath, JSON.stringify(nextConfig, null, 2), { mode: 0o600 });
|
|
34
34
|
fs.chmodSync(configPath, 0o600);
|
|
35
|
-
|
|
35
|
+
info(`🔑 ${env} 环境认证令牌已安全存储`);
|
|
36
36
|
} catch (err) {
|
|
37
37
|
throw new Error(`保存 ${env} 环境认证令牌失败:${err.message}`);
|
|
38
38
|
}
|
package/utils/common.js
CHANGED
|
@@ -68,20 +68,6 @@ export const isUrlAccessible = async (url) => {
|
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
/** 重命名文件夹
|
|
72
|
-
* @param {*} dirname 旧文件夹名
|
|
73
|
-
* @param {*} newname 新文件夹名
|
|
74
|
-
*/
|
|
75
|
-
export async function renameDistFolder(dirname, newname) {
|
|
76
|
-
try {
|
|
77
|
-
if (fs.existsSync(newname)) return true;
|
|
78
|
-
fs.renameSync(dirname, newname);
|
|
79
|
-
return true;
|
|
80
|
-
} catch (e) {
|
|
81
|
-
error("重命名文件夹失败:", e);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
71
|
/** 创建工作文件夹
|
|
86
72
|
* @param {*} dirname 文件夹名
|
|
87
73
|
*/
|
|
@@ -105,69 +91,15 @@ export async function createWorkSpaceFolder(dirname = "") {
|
|
|
105
91
|
// 压缩项目
|
|
106
92
|
async function compressProject(output, dirname = "", loader, appId) {
|
|
107
93
|
try {
|
|
108
|
-
if (loader !== "EMP")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!fs.existsSync(h5Path)) {
|
|
113
|
-
throw new Error("h5 目录不存在");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 添加文件夹时指定目标路径为 h5
|
|
117
|
-
zip.addLocalFolder(h5Path, "h5");
|
|
118
|
-
zip.writeZip(path.join(dirname, `${output}.zip`));
|
|
119
|
-
} else {
|
|
120
|
-
// 直接压缩dist
|
|
121
|
-
const zip = new AdmZip();
|
|
122
|
-
zip.addLocalFolder(path.join(dirname, "dist"), `${appId}`);
|
|
123
|
-
zip.writeZip(path.join(dirname, `${output}.zip`));
|
|
124
|
-
}
|
|
94
|
+
if (loader !== "EMP") throw new Error("仅支持 EMP 产物");
|
|
95
|
+
const zip = new AdmZip();
|
|
96
|
+
zip.addLocalFolder(path.join(dirname, "dist"), `${appId}`);
|
|
97
|
+
zip.writeZip(path.join(dirname, `${output}.zip`));
|
|
125
98
|
return true;
|
|
126
99
|
} catch (error) {
|
|
127
100
|
return false;
|
|
128
101
|
}
|
|
129
102
|
}
|
|
130
|
-
/** 复制文件夹
|
|
131
|
-
* @param {*} dirname 文件夹名
|
|
132
|
-
*/
|
|
133
|
-
async function copyDistFolder(dirname) {
|
|
134
|
-
try {
|
|
135
|
-
info("正在查找 index.html 文件...");
|
|
136
|
-
let indexPath = null;
|
|
137
|
-
function findIndexHtml(dir) {
|
|
138
|
-
const files = fs.readdirSync(dir);
|
|
139
|
-
for (const file of files) {
|
|
140
|
-
const fullPath = path.join(dir, file);
|
|
141
|
-
const stat = fs.statSync(fullPath);
|
|
142
|
-
if (stat.isDirectory()) {
|
|
143
|
-
const found = findIndexHtml(fullPath);
|
|
144
|
-
if (found) return found;
|
|
145
|
-
} else if (file === "index.html") {
|
|
146
|
-
return fullPath;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
indexPath = findIndexHtml(dirname);
|
|
152
|
-
if (!indexPath) {
|
|
153
|
-
throw new Error("未找到index.html文件");
|
|
154
|
-
}
|
|
155
|
-
success("找到 index.html 文件");
|
|
156
|
-
|
|
157
|
-
const parentDir = path.dirname(indexPath);
|
|
158
|
-
const targetDir = path.join(WORKSPACE_DIR, "dist");
|
|
159
|
-
|
|
160
|
-
info("正在复制文件...");
|
|
161
|
-
// 直接复制整个目录
|
|
162
|
-
fs.cpSync(parentDir, targetDir, { recursive: true });
|
|
163
|
-
success("文件复制完成");
|
|
164
|
-
return true;
|
|
165
|
-
} catch (e) {
|
|
166
|
-
error(e);
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
103
|
function hasCompiledMainDir(dirname) {
|
|
172
104
|
try {
|
|
173
105
|
return fs.statSync(path.join(dirname, "main")).isDirectory();
|
|
@@ -250,7 +182,6 @@ async function copyAndSetEmpDistFolder(appId, code) {
|
|
|
250
182
|
* 预上传准备工作
|
|
251
183
|
* 1. 创建工作空间
|
|
252
184
|
* 2. 复制构建产物
|
|
253
|
-
* 3. 重命名文件夹(非EMP构建模式)
|
|
254
185
|
* 4. 压缩项目
|
|
255
186
|
* @param {string} loader - 构建加载器类型
|
|
256
187
|
* @param {string} targetName - 目标文件名
|
|
@@ -258,6 +189,7 @@ async function copyAndSetEmpDistFolder(appId, code) {
|
|
|
258
189
|
*/
|
|
259
190
|
export async function preUpload(loader, targetName, appId, code) {
|
|
260
191
|
try {
|
|
192
|
+
if (loader !== "EMP") throw new Error("仅支持 2.0(EMP)小程序上传");
|
|
261
193
|
// 1. 创建工作空间文件夹
|
|
262
194
|
if (!(await createWorkSpaceFolder(WORKSPACE_DIR))) {
|
|
263
195
|
error("创建工作空间失败");
|
|
@@ -265,10 +197,7 @@ export async function preUpload(loader, targetName, appId, code) {
|
|
|
265
197
|
}
|
|
266
198
|
|
|
267
199
|
// 2. 根据构建类型复制对应产物
|
|
268
|
-
const isCopySuccess =
|
|
269
|
-
loader === "EMP"
|
|
270
|
-
? await copyAndSetEmpDistFolder(appId, code)
|
|
271
|
-
: await copyAndRenameDistFolder();
|
|
200
|
+
const isCopySuccess = await copyAndSetEmpDistFolder(appId, code);
|
|
272
201
|
|
|
273
202
|
if (!isCopySuccess) {
|
|
274
203
|
error("复制构建产物失败");
|
|
@@ -278,7 +207,7 @@ export async function preUpload(loader, targetName, appId, code) {
|
|
|
278
207
|
// 3. 复制 qdmp.json 到产物目录
|
|
279
208
|
const qdmpJsonPath = path.resolve(process.cwd(), "qdmp.json");
|
|
280
209
|
if (fs.existsSync(qdmpJsonPath)) {
|
|
281
|
-
const distDir =
|
|
210
|
+
const distDir = "dist";
|
|
282
211
|
fs.copyFileSync(qdmpJsonPath, path.join(WORKSPACE_DIR, distDir, "qdmp.json"));
|
|
283
212
|
info("已将 qdmp.json 复制到产物目录");
|
|
284
213
|
}
|
|
@@ -299,23 +228,6 @@ export async function preUpload(loader, targetName, appId, code) {
|
|
|
299
228
|
}
|
|
300
229
|
}
|
|
301
230
|
|
|
302
|
-
/**
|
|
303
|
-
* 复制并创建标准H5文件夹结构
|
|
304
|
-
* 对于非EMP构建模式,复制dist内容并重命名为h5
|
|
305
|
-
* @returns {Promise<boolean>} 操作是否成功
|
|
306
|
-
*/
|
|
307
|
-
async function copyAndRenameDistFolder() {
|
|
308
|
-
try {
|
|
309
|
-
if (!(await copyDistFolder("dist"))) {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return renameDistFolder(`${WORKSPACE_DIR}/dist`, `${WORKSPACE_DIR}/h5`);
|
|
314
|
-
} catch (err) {
|
|
315
|
-
error("复制并重命名文件夹过程出错:", err);
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
231
|
/** 清理工作文件夹
|
|
320
232
|
* @param {*} dirname 文件夹名
|
|
321
233
|
*/
|
package/utils/gitClone.js
CHANGED
|
@@ -23,15 +23,16 @@ export const download = async (projectName, repo) => {
|
|
|
23
23
|
`文件夹 ${projectName} 已存在,是否创建新名称: ${name}`
|
|
24
24
|
);
|
|
25
25
|
if (confirm) {
|
|
26
|
-
clone(repo, name);
|
|
26
|
+
await clone(repo, name);
|
|
27
27
|
} else {
|
|
28
28
|
error(`文件夹 ${projectName} 已存在, 请更名后重新创建`);
|
|
29
29
|
}
|
|
30
30
|
} else {
|
|
31
|
-
clone(repo, name);
|
|
31
|
+
await clone(repo, name);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
return path.join(process.cwd(), name);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
throw err;
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
|
|
@@ -81,5 +82,6 @@ const clone = async (repo, name) => {
|
|
|
81
82
|
spinner.succeed(chalk.green(`模版创建成功!请进入 ${name} 目录开始开发`));
|
|
82
83
|
} catch (err) {
|
|
83
84
|
spinner.fail(chalk.red(`模版创建失败: ${err.message}`));
|
|
85
|
+
throw err;
|
|
84
86
|
}
|
|
85
87
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function readProjectLoader(cwd = process.cwd()) {
|
|
5
|
+
const config = JSON.parse(fs.readFileSync(path.join(cwd, "qdmp.json"), "utf8"));
|
|
6
|
+
if (config.loader !== "EMP") {
|
|
7
|
+
throw new Error("仅支持 2.0(EMP)小程序;qdmp.json 的 loader 必须为 EMP。不支持 SPA/effuse 工程,不能只修改 loader 转换工程。");
|
|
8
|
+
}
|
|
9
|
+
const packagePath = path.join(cwd, "package.json");
|
|
10
|
+
if (fs.existsSync(packagePath)) {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
12
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
13
|
+
for (const [name, version] of Object.entries(pkg[field] || {})) {
|
|
14
|
+
if (/effuse|effues/i.test(name) || /effuse|effues/i.test(String(version))) {
|
|
15
|
+
throw new Error(`不支持 effuse/effues 依赖:${name}。请使用官方 2.0 模板迁移业务,不要仅修改 loader。`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return config.loader;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertMatchingLoader(localLoader, remoteLoader) {
|
|
24
|
+
if (localLoader !== "EMP" || remoteLoader !== "EMP") {
|
|
25
|
+
throw new Error(`应用类型不一致或不受支持(仅支持 EMP):本地 ${localLoader},平台 ${remoteLoader ?? "未提供"}。请关联匹配的应用或完成工程迁移,不能仅修改 loader。`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertEMPTemplate(cwd) {
|
|
30
|
+
if (readProjectLoader(cwd) !== "EMP") {
|
|
31
|
+
throw new Error("创建结果不是 2.0(EMP)模板,请检查官方模板来源。");
|
|
32
|
+
}
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
34
|
+
if (!pkg.scripts?.build) throw new Error("模板缺少 build 脚本。");
|
|
35
|
+
// package.json.version 是业务包版本,不是小程序运行时版本。
|
|
36
|
+
}
|
package/utils/agentLogin.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, chmod, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import QRCode from "qrcode";
|
|
6
|
-
import { generateLoginQrcode, getLoginQrcodeStatus, getQiandaoUser, exchangeQiandaoLogin, request } from "../api.js";
|
|
7
|
-
import { saveToken } from "./authorized.js";
|
|
8
|
-
import { normalizeLoginEnv } from "./loginEnvironment.js";
|
|
9
|
-
import { startLocalhostLogin } from "./localhostLogin.js";
|
|
10
|
-
|
|
11
|
-
// NDJSON keeps stdout machine-readable. The caller renders ready.markdown while
|
|
12
|
-
// this process keeps the loopback callback and App status polling alive.
|
|
13
|
-
export const agentLogin = async (env = "prod", {
|
|
14
|
-
emit = event => console.log(JSON.stringify(event)),
|
|
15
|
-
timeoutMs = 120000,
|
|
16
|
-
pollIntervalMs = 1500,
|
|
17
|
-
} = {}) => {
|
|
18
|
-
normalizeLoginEnv(env);
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const { signal } = controller;
|
|
21
|
-
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
22
|
-
const timer = setTimeout(() => controller.abort(new Error("登录超时,请重新执行登录命令")), timeoutMs);
|
|
23
|
-
const interrupt = () => controller.abort(new Error("已取消登录"));
|
|
24
|
-
process.once("SIGINT", interrupt);
|
|
25
|
-
process.once("SIGTERM", interrupt);
|
|
26
|
-
let browserSession;
|
|
27
|
-
let imageDirectory;
|
|
28
|
-
let committed = false;
|
|
29
|
-
const closeBrowser = () => browserSession?.close();
|
|
30
|
-
signal.addEventListener("abort", closeBrowser, { once: true });
|
|
31
|
-
const commit = token => {
|
|
32
|
-
signal.throwIfAborted();
|
|
33
|
-
if (committed) throw new Error("登录已完成");
|
|
34
|
-
saveToken(token, env, { quiet: true });
|
|
35
|
-
committed = true;
|
|
36
|
-
};
|
|
37
|
-
const requestSignal = () => AbortSignal.any([signal, AbortSignal.timeout(10000)]);
|
|
38
|
-
try {
|
|
39
|
-
browserSession = await startLocalhostLogin({
|
|
40
|
-
env, timeoutMs,
|
|
41
|
-
acceptToken: async (token, isActive) => {
|
|
42
|
-
const result = await request("/mp/v1/user/me", {
|
|
43
|
-
headers: { authorization: `Bearer ${token}` }, signal: requestSignal(),
|
|
44
|
-
}, env, false);
|
|
45
|
-
if (Number(result?.code || 0) !== 0) throw new Error("验证网页登录态失败");
|
|
46
|
-
if (!isActive()) throw new Error("网页登录会话已结束");
|
|
47
|
-
commit(token);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
signal.throwIfAborted();
|
|
51
|
-
const qr = await generateLoginQrcode(env, { signal: requestSignal() });
|
|
52
|
-
imageDirectory = await mkdtemp(join(tmpdir(), "qdmp-login-"));
|
|
53
|
-
const qrImagePath = join(imageDirectory, "qrcode.png");
|
|
54
|
-
await QRCode.toFile(qrImagePath, qr.url, { type: "png", width: 320, margin: 4 });
|
|
55
|
-
await chmod(qrImagePath, 0o600);
|
|
56
|
-
signal.throwIfAborted();
|
|
57
|
-
const loginUrl = browserSession.url;
|
|
58
|
-
await emit({
|
|
59
|
-
type: "ready", env, loginUrl, qrImagePath, expiresAt,
|
|
60
|
-
message: "点击链接前往浏览器登录,或用千岛 App 扫码登录。",
|
|
61
|
-
markdown: `[点击链接前往浏览器登录](${loginUrl}),或用千岛 App 扫码登录。\n\n`,
|
|
62
|
-
});
|
|
63
|
-
const poll = async () => {
|
|
64
|
-
let previousStatus;
|
|
65
|
-
while (!signal.aborted && !committed) {
|
|
66
|
-
const result = await getLoginQrcodeStatus(qr.qrcodeId, env, { signal: requestSignal() });
|
|
67
|
-
signal.throwIfAborted();
|
|
68
|
-
if (result.status === "success") {
|
|
69
|
-
if (!result.token) throw new Error("扫码登录未返回登录态");
|
|
70
|
-
const user = await getQiandaoUser(result.token, env, { signal: requestSignal() });
|
|
71
|
-
const token = await exchangeQiandaoLogin(result.token, user, env, { signal: requestSignal(), persist: false });
|
|
72
|
-
commit(token);
|
|
73
|
-
return "qrcode";
|
|
74
|
-
}
|
|
75
|
-
if (["expired", "canceled", "cancelled", "error"].includes(result.status)) {
|
|
76
|
-
throw new Error("二维码已过期或登录已取消,可继续使用浏览器链接");
|
|
77
|
-
}
|
|
78
|
-
if (previousStatus !== result.status) {
|
|
79
|
-
previousStatus = result.status;
|
|
80
|
-
await emit({ type: "status", env, method: "qrcode", status: result.status });
|
|
81
|
-
}
|
|
82
|
-
await delay(pollIntervalMs, undefined, { signal });
|
|
83
|
-
}
|
|
84
|
-
throw new Error("扫码登录会话已结束");
|
|
85
|
-
};
|
|
86
|
-
const lane = (promise, method) => promise.catch(async () => {
|
|
87
|
-
if (!signal.aborted && !committed) await emit({ type: "unavailable", env, method });
|
|
88
|
-
throw new Error(`${method} 登录未完成`);
|
|
89
|
-
});
|
|
90
|
-
const method = await Promise.any([
|
|
91
|
-
lane(browserSession.result.then(() => "browser"), "browser"),
|
|
92
|
-
lane(poll(), "qrcode"),
|
|
93
|
-
]).catch(() => {
|
|
94
|
-
throw signal.reason || new Error("登录未完成,请重新执行登录命令");
|
|
95
|
-
});
|
|
96
|
-
await emit({ type: "success", env, method });
|
|
97
|
-
} finally {
|
|
98
|
-
controller.abort();
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
closeBrowser();
|
|
101
|
-
process.removeListener("SIGINT", interrupt);
|
|
102
|
-
process.removeListener("SIGTERM", interrupt);
|
|
103
|
-
signal.removeEventListener("abort", closeBrowser);
|
|
104
|
-
if (imageDirectory) await rm(imageDirectory, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
};
|