@applift/core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{plugin-CTt0LO8c.d.mts → frameworks-D83HVg8t.d.mts} +120 -59
- package/dist/index.d.mts +71 -10
- package/dist/index.mjs +481 -903
- package/dist/plugins/index.d.mts +74 -3
- package/dist/plugins/index.mjs +437 -2
- package/dist/project-4_orxby_.mjs +806 -0
- package/package.json +1 -1
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { E as FrameworkContext, O as UpdateProvider, _ as AppliftPlugin, a as knownFrameworkAdapters, c as nativeHarmonyAdapter, d as nativeIosFramework, f as rnAdapter, g as tauriFramework, h as tauriAdapter, i as flutterFramework, l as nativeHarmonyFramework, m as suggestFramework, n as electronFramework, o as nativeAndroidAdapter, p as rnFramework, r as flutterAdapter, s as nativeAndroidFramework, t as electronAdapter, u as nativeIosAdapter, w as DeliveryJudgement } from "../frameworks-D83HVg8t.mjs";
|
|
2
2
|
//#region src/plugins/dingtalk-notify.d.ts
|
|
3
3
|
interface DingtalkNotifyOptions {
|
|
4
4
|
webhook: string;
|
|
@@ -6,6 +6,77 @@ interface DingtalkNotifyOptions {
|
|
|
6
6
|
secret?: string;
|
|
7
7
|
atMobiles?: string[];
|
|
8
8
|
}
|
|
9
|
-
export declare function dingtalkNotify(options: DingtalkNotifyOptions):
|
|
9
|
+
export declare function dingtalkNotify(options: DingtalkNotifyOptions): AppliftPlugin;
|
|
10
10
|
//#endregion
|
|
11
|
-
|
|
11
|
+
//#region src/plugins/pushy-cli.d.ts
|
|
12
|
+
interface CliResult {
|
|
13
|
+
code: number;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
type CliRunner = (args: string[], opts?: {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
}) => Promise<CliResult>;
|
|
20
|
+
/** 默认 runner:spawn 全局 pushy,超时 10 分钟(bundle/upload 含网络大文件传输) */
|
|
21
|
+
export declare function spawnPushy(args: string[], opts?: {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}): Promise<CliResult>;
|
|
24
|
+
interface PushyCli {
|
|
25
|
+
/** 构建热更包,返回 ppk 路径 */
|
|
26
|
+
bundle(platform: string, cwd?: string): Promise<string>;
|
|
27
|
+
/** 发布热更版本(publish),返回版本 id */
|
|
28
|
+
publish(input: {
|
|
29
|
+
appId: number;
|
|
30
|
+
ppk: string;
|
|
31
|
+
platform: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
packageVersion: string;
|
|
35
|
+
rollout?: number;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
versionId: number;
|
|
38
|
+
}>;
|
|
39
|
+
/** 上传原生基线(基线版本号由服务端从包内读取) */
|
|
40
|
+
uploadApp(appId: number, appFile: string): Promise<{
|
|
41
|
+
packageId: number;
|
|
42
|
+
version: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** 版本重绑(promote 语义:测试通道版本重绑正式原生版本 + 灰度) */
|
|
45
|
+
rebind(input: {
|
|
46
|
+
appId: number;
|
|
47
|
+
versionId: number;
|
|
48
|
+
packageVersion: string;
|
|
49
|
+
rollout?: number;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
/** 版本列表(取号与状态展示用) */
|
|
52
|
+
versions(appId: number): Promise<Array<{
|
|
53
|
+
id: number;
|
|
54
|
+
hash: string;
|
|
55
|
+
name: string;
|
|
56
|
+
}>>;
|
|
57
|
+
}
|
|
58
|
+
export declare function createPushyCli(runner?: CliRunner): PushyCli;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/plugins/rn-update.d.ts
|
|
61
|
+
interface RnUpdateOptions {
|
|
62
|
+
/** 各平台的 app 记录号(如 harmony: 34166);未声明的平台不启用热更 */
|
|
63
|
+
harmony?: number;
|
|
64
|
+
android?: number;
|
|
65
|
+
ios?: number;
|
|
66
|
+
/** 补丁目录(patch-package 约定默认 patches/,项目可覆盖) */
|
|
67
|
+
patchDir?: string;
|
|
68
|
+
/** 正式发布默认灰度比例 */
|
|
69
|
+
defaultRollout?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 判定引擎:diff 能否走热更。
|
|
73
|
+
* 五形态完备性:新原生实体必现于 原生目录/依赖(原生包)/补丁/构建配置/JS——前四判原生,纯 JS 判热更,其余灰区。
|
|
74
|
+
*/
|
|
75
|
+
export declare function judgeDelivery(diff: string[], ctx: FrameworkContext, opts: RnUpdateOptions): DeliveryJudgement;
|
|
76
|
+
/** provider 构造(cli 注入点:测试 mock / 将来直连 API) */
|
|
77
|
+
export declare function makeRnUpdateProvider(opts: RnUpdateOptions, cli?: PushyCli): UpdateProvider;
|
|
78
|
+
/** rnUpdate 插件工厂:注册完整 provider(判定 + 测试发布 + promote)+ 原生发版自动基线。
|
|
79
|
+
* cli 注入点:测试 mock / 将来进程内直连 API。 */
|
|
80
|
+
export declare function rnUpdate(opts?: RnUpdateOptions, cli?: PushyCli): AppliftPlugin;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { type CliResult, type CliRunner, type DingtalkNotifyOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { _ as suggestFramework, a as electronFramework, c as knownFrameworkAdapters, d as nativeHarmonyAdapter, f as nativeHarmonyFramework, g as rnFramework, h as rnAdapter, i as electronAdapter, l as nativeAndroidAdapter, m as nativeIosFramework, o as flutterAdapter, p as nativeIosAdapter, r as redactText, s as flutterFramework, t as readProjectVersions, u as nativeAndroidFramework, v as tauriAdapter, y as tauriFramework } from "../project-4_orxby_.mjs";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { createHmac } from "node:crypto";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
6
|
//#region src/plugins/dingtalk-notify.ts
|
|
3
7
|
/**
|
|
4
8
|
* 官方示例插件 —— 钉钉机器人通知。
|
|
@@ -13,7 +17,7 @@ function dingtalkNotify(options) {
|
|
|
13
17
|
async notify(summary) {
|
|
14
18
|
const title = summary.submit.length === 0 ? "applift stage 草稿(未提审)" : "applift 发布结果";
|
|
15
19
|
const lines = [title];
|
|
16
|
-
for (const e of [...summary.stage, ...summary.submit]) lines.push(`- ${e.label}: ${e.ok ? "✅ 成功" : `❌ ${e.stageError ?? e.submitError ?? "失败"}`}`);
|
|
20
|
+
for (const e of [...summary.stage, ...summary.submit]) lines.push(`- ${e.label}: ${e.ok ? "✅ 成功" : `❌ ${redactText(e.stageError ?? e.submitError ?? "失败")}`}`);
|
|
17
21
|
const body = {
|
|
18
22
|
msgtype: "markdown",
|
|
19
23
|
markdown: {
|
|
@@ -41,4 +45,435 @@ function dingtalkNotify(options) {
|
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
//#endregion
|
|
44
|
-
|
|
48
|
+
//#region src/plugins/pushy-cli.ts
|
|
49
|
+
/**
|
|
50
|
+
* 不 require 其内部 lib(无公开契约);cresc/自建服务的差异在服务端 URL,CLI 参数面一致。
|
|
51
|
+
* 输出解析正则按 react-native-update-cli 2.22.9 实测文案编写(源码 i18n 硬编码:pushy 恒中文、cresc 恒英文,
|
|
52
|
+
* 不随系统 locale);CLI 升级改文案会显式抛错而非静默,升级后请跑 pushy-cli.test.ts 的真实输出回归。
|
|
53
|
+
* cresc 二进制输出为英文,本封装不适用——换 cresc 应另写 provider 插件而非复用本封装。
|
|
54
|
+
*/
|
|
55
|
+
/** 默认 runner:spawn 全局 pushy,超时 10 分钟(bundle/upload 含网络大文件传输) */
|
|
56
|
+
function spawnPushy(args, opts) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn("pushy", args, { cwd: opts?.cwd });
|
|
59
|
+
let stdout = "";
|
|
60
|
+
let stderr = "";
|
|
61
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 6e5);
|
|
62
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
63
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
64
|
+
child.on("error", (e) => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
reject(/* @__PURE__ */ new Error(`pushy 命令不可用(react-native-update-cli 未全局安装?):${e.message}`));
|
|
67
|
+
});
|
|
68
|
+
child.on("close", (code) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
resolve({
|
|
71
|
+
code: code ?? 1,
|
|
72
|
+
stdout,
|
|
73
|
+
stderr
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function fail(cmd, r) {
|
|
79
|
+
throw new Error(`pushy ${cmd} 失败(exit ${r.code}):${(r.stderr || r.stdout).trim()}`);
|
|
80
|
+
}
|
|
81
|
+
function createPushyCli(runner = spawnPushy) {
|
|
82
|
+
return {
|
|
83
|
+
async bundle(platform, cwd) {
|
|
84
|
+
const r = await runner([
|
|
85
|
+
"bundle",
|
|
86
|
+
"--platform",
|
|
87
|
+
platform,
|
|
88
|
+
"--no-interactive"
|
|
89
|
+
], { cwd });
|
|
90
|
+
if (r.code !== 0) fail("bundle", r);
|
|
91
|
+
const m = r.stdout.match(/已生成\s+(\S+\.ppk)/);
|
|
92
|
+
if (!m) throw new Error(`pushy bundle 输出未含 ppk 路径:${r.stdout.trim()}`);
|
|
93
|
+
return m[1];
|
|
94
|
+
},
|
|
95
|
+
async publish(input) {
|
|
96
|
+
const args = [
|
|
97
|
+
"publish",
|
|
98
|
+
input.ppk,
|
|
99
|
+
"--platform",
|
|
100
|
+
input.platform,
|
|
101
|
+
"--appId",
|
|
102
|
+
String(input.appId),
|
|
103
|
+
"--name",
|
|
104
|
+
input.name,
|
|
105
|
+
"--packageVersion",
|
|
106
|
+
input.packageVersion
|
|
107
|
+
];
|
|
108
|
+
if (input.description) args.push("--description", input.description);
|
|
109
|
+
if (input.rollout !== void 0) args.push("--rollout", String(input.rollout));
|
|
110
|
+
args.push("--no-interactive");
|
|
111
|
+
const r = await runner(args);
|
|
112
|
+
if (r.code !== 0) fail("publish", r);
|
|
113
|
+
const m = (r.stdout + r.stderr).match(/已成功上传新热更包(id:\s*(\d+))/);
|
|
114
|
+
if (!m) throw new Error(`pushy publish 输出未含版本 id:${r.stdout.trim()}`);
|
|
115
|
+
return { versionId: Number(m[1]) };
|
|
116
|
+
},
|
|
117
|
+
async uploadApp(appId, appFile) {
|
|
118
|
+
const r = await runner([
|
|
119
|
+
"uploadApp",
|
|
120
|
+
appFile,
|
|
121
|
+
"--appId",
|
|
122
|
+
String(appId)
|
|
123
|
+
]);
|
|
124
|
+
if (r.code !== 0) fail("uploadApp", r);
|
|
125
|
+
const m = (r.stdout + r.stderr).match(/已成功上传app原生包(id:\s*(\d+),\s*version:\s*([\d.]+)[^)]*)/);
|
|
126
|
+
if (!m) throw new Error(`pushy uploadApp 输出未含原生包信息:${(r.stdout + r.stderr).trim()}`);
|
|
127
|
+
return {
|
|
128
|
+
packageId: Number(m[1]),
|
|
129
|
+
version: m[2]
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async rebind(input) {
|
|
133
|
+
const args = [
|
|
134
|
+
"update",
|
|
135
|
+
"--versionId",
|
|
136
|
+
String(input.versionId),
|
|
137
|
+
"--appId",
|
|
138
|
+
String(input.appId),
|
|
139
|
+
"--packageVersion",
|
|
140
|
+
input.packageVersion
|
|
141
|
+
];
|
|
142
|
+
if (input.rollout !== void 0) args.push("--rollout", String(input.rollout));
|
|
143
|
+
args.push("--no-interactive");
|
|
144
|
+
const r = await runner(args);
|
|
145
|
+
if (r.code !== 0) fail("update", r);
|
|
146
|
+
},
|
|
147
|
+
async versions(appId) {
|
|
148
|
+
const r = await runner([
|
|
149
|
+
"versions",
|
|
150
|
+
"--appId",
|
|
151
|
+
String(appId),
|
|
152
|
+
"--no-interactive"
|
|
153
|
+
]);
|
|
154
|
+
if (r.code !== 0) fail("versions", r);
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const m of r.stdout.matchAll(/^(\d+)\)\s+(\S+)\s+(\S+)/gm)) out.push({
|
|
157
|
+
id: Number(m[1]),
|
|
158
|
+
hash: m[2],
|
|
159
|
+
name: m[3]
|
|
160
|
+
});
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/plugins/rn-update.ts
|
|
167
|
+
/**
|
|
168
|
+
* rn-update 插件:react-native-update(pushy/cresc/自建服务端)的 applift UpdateProvider 实现。
|
|
169
|
+
* 判定引擎为纯函数(五形态完备性见测试头注释);发布链路 = ppk 构建 / 测试通道 / promote / 原生基线。
|
|
170
|
+
*/
|
|
171
|
+
/** 项目原生目录:任一命中即必须原生发版 */
|
|
172
|
+
const NATIVE_DIRS = [
|
|
173
|
+
"harmony/",
|
|
174
|
+
"ios/",
|
|
175
|
+
"android/"
|
|
176
|
+
];
|
|
177
|
+
/** 根级构建/身份配置:改变产物生成方式或应用标识,保守判原生 */
|
|
178
|
+
const NATIVE_ROOT_FILES = /* @__PURE__ */ new Set([
|
|
179
|
+
"app.json",
|
|
180
|
+
"react-native.config.js",
|
|
181
|
+
"react-native.config.cjs",
|
|
182
|
+
"metro.config.js",
|
|
183
|
+
"metro.config.cjs",
|
|
184
|
+
"metro.config.ts",
|
|
185
|
+
"babel.config.js",
|
|
186
|
+
"babel.config.cjs"
|
|
187
|
+
]);
|
|
188
|
+
function classifyPath(file, opts) {
|
|
189
|
+
if (NATIVE_DIRS.some((d) => file.startsWith(d))) return {
|
|
190
|
+
kind: "native",
|
|
191
|
+
rule: "原生目录"
|
|
192
|
+
};
|
|
193
|
+
const patchDir = opts.patchDir ?? "patches";
|
|
194
|
+
if (file.startsWith(`${patchDir}/`)) return {
|
|
195
|
+
kind: "native",
|
|
196
|
+
rule: `补丁目录(${patchDir}/)`
|
|
197
|
+
};
|
|
198
|
+
if (NATIVE_ROOT_FILES.has(file)) return {
|
|
199
|
+
kind: "native",
|
|
200
|
+
rule: "构建/身份配置"
|
|
201
|
+
};
|
|
202
|
+
if (file === "package.json") return {
|
|
203
|
+
kind: "dep-analysis",
|
|
204
|
+
rule: "依赖变更"
|
|
205
|
+
};
|
|
206
|
+
if (file === "package-lock.json") return {
|
|
207
|
+
kind: "uncertain",
|
|
208
|
+
rule: "lockfile 变更(依赖树内部变化)"
|
|
209
|
+
};
|
|
210
|
+
if (file.startsWith("src/")) return {
|
|
211
|
+
kind: "js",
|
|
212
|
+
rule: "JS 层"
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
kind: "uncertain",
|
|
216
|
+
rule: "未知路径(灰区)"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** 读取 package.json 的依赖集合(deps + devDeps 的 name→range 映射) */
|
|
220
|
+
function readDeps(file) {
|
|
221
|
+
try {
|
|
222
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
223
|
+
return {
|
|
224
|
+
...pkg.dependencies,
|
|
225
|
+
...pkg.devDependencies
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** node_modules 原生信号检测:true=含原生实现;false=纯 JS;null=无法检测(包未安装) */
|
|
232
|
+
function hasNativeSignal(cwd, name) {
|
|
233
|
+
if (name.startsWith("@react-native-ohos/")) return true;
|
|
234
|
+
const root = path.join(cwd, "node_modules", name);
|
|
235
|
+
const pkgFile = path.join(root, "package.json");
|
|
236
|
+
try {
|
|
237
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
238
|
+
if ("react-native" in pkg || "harmony" in pkg) return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (fs.readdirSync(root, { withFileTypes: true }).some((e) => e.isFile() && e.name.endsWith(".podspec"))) return true;
|
|
243
|
+
return fs.existsSync(path.join(root, "android", "build.gradle"));
|
|
244
|
+
}
|
|
245
|
+
/** 锚点..当前的 package.json 依赖对比:返回变更包名列表(无锚点返回 null) */
|
|
246
|
+
function changedPackages(cwd, anchor) {
|
|
247
|
+
if (!anchor) return null;
|
|
248
|
+
const old = readDepsFromGit(cwd, anchor);
|
|
249
|
+
const cur = readDeps(path.join(cwd, "package.json"));
|
|
250
|
+
if (old === null) return null;
|
|
251
|
+
const changed = [];
|
|
252
|
+
for (const [name, range] of Object.entries(cur)) if (!(name in old) || old[name] !== range) changed.push(name);
|
|
253
|
+
for (const name of Object.keys(old)) if (!(name in cur)) changed.push(name);
|
|
254
|
+
return changed;
|
|
255
|
+
}
|
|
256
|
+
function readDepsFromGit(cwd, ref) {
|
|
257
|
+
const r = spawnSync("git", ["show", `${ref}:package.json`], {
|
|
258
|
+
cwd,
|
|
259
|
+
encoding: "utf8"
|
|
260
|
+
});
|
|
261
|
+
if (r.status !== 0) return null;
|
|
262
|
+
try {
|
|
263
|
+
const pkg = JSON.parse(r.stdout);
|
|
264
|
+
return {
|
|
265
|
+
...pkg.dependencies,
|
|
266
|
+
...pkg.devDependencies
|
|
267
|
+
};
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 判定引擎:diff 能否走热更。
|
|
274
|
+
* 五形态完备性:新原生实体必现于 原生目录/依赖(原生包)/补丁/构建配置/JS——前四判原生,纯 JS 判热更,其余灰区。
|
|
275
|
+
*/
|
|
276
|
+
function judgeDelivery(diff, ctx, opts) {
|
|
277
|
+
const evidence = [];
|
|
278
|
+
let hasNative = false;
|
|
279
|
+
let hasUncertain = false;
|
|
280
|
+
let hasJs = false;
|
|
281
|
+
if (diff.length === 0) return {
|
|
282
|
+
recommendation: "uncertain",
|
|
283
|
+
confidence: "low",
|
|
284
|
+
evidence: [{
|
|
285
|
+
path: "(无 diff)",
|
|
286
|
+
rule: "锚点缺失或无变更",
|
|
287
|
+
note: "无锚点 tag 时无法判定,请人工确认"
|
|
288
|
+
}]
|
|
289
|
+
};
|
|
290
|
+
for (const file of diff) {
|
|
291
|
+
const verdict = classifyPath(file, opts);
|
|
292
|
+
if (verdict.kind === "dep-analysis") {
|
|
293
|
+
const changed = changedPackages(ctx.cwd, ctx.anchor);
|
|
294
|
+
if (changed === null) {
|
|
295
|
+
evidence.push({
|
|
296
|
+
path: file,
|
|
297
|
+
rule: "依赖变更(无锚点)",
|
|
298
|
+
note: "无法对比,请人工确认"
|
|
299
|
+
});
|
|
300
|
+
hasUncertain = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (changed.length === 0) {
|
|
304
|
+
evidence.push({
|
|
305
|
+
path: file,
|
|
306
|
+
rule: "构建/身份配置",
|
|
307
|
+
note: "依赖未变(仅元数据),保守判原生"
|
|
308
|
+
});
|
|
309
|
+
hasNative = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const signals = changed.map((n) => ({
|
|
313
|
+
name: n,
|
|
314
|
+
signal: hasNativeSignal(ctx.cwd, n)
|
|
315
|
+
}));
|
|
316
|
+
const nativePkgs = signals.filter((s) => s.signal === true).map((s) => s.name);
|
|
317
|
+
const unknownPkgs = signals.filter((s) => s.signal === null).map((s) => s.name);
|
|
318
|
+
const jsOnly = signals.filter((s) => s.signal === false).map((s) => s.name);
|
|
319
|
+
if (nativePkgs.length > 0) {
|
|
320
|
+
evidence.push({
|
|
321
|
+
path: file,
|
|
322
|
+
rule: "依赖变更(含原生包)",
|
|
323
|
+
note: `原生:${nativePkgs.join(", ")}`
|
|
324
|
+
});
|
|
325
|
+
hasNative = true;
|
|
326
|
+
} else if (unknownPkgs.length > 0) {
|
|
327
|
+
evidence.push({
|
|
328
|
+
path: file,
|
|
329
|
+
rule: "依赖变更(未安装,无法检测)",
|
|
330
|
+
note: `请先安装依赖再判定:${unknownPkgs.join(", ")}`
|
|
331
|
+
});
|
|
332
|
+
hasUncertain = true;
|
|
333
|
+
} else {
|
|
334
|
+
evidence.push({
|
|
335
|
+
path: file,
|
|
336
|
+
rule: "依赖变更(纯 JS 包)",
|
|
337
|
+
note: `纯 JS:${jsOnly.join(", ")}`
|
|
338
|
+
});
|
|
339
|
+
hasJs = true;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
evidence.push({
|
|
344
|
+
path: file,
|
|
345
|
+
rule: verdict.rule,
|
|
346
|
+
note: verdict.note ?? ""
|
|
347
|
+
});
|
|
348
|
+
if (verdict.kind === "native") hasNative = true;
|
|
349
|
+
else if (verdict.kind === "uncertain") hasUncertain = true;
|
|
350
|
+
else hasJs = true;
|
|
351
|
+
}
|
|
352
|
+
if (hasNative) return {
|
|
353
|
+
recommendation: "native",
|
|
354
|
+
confidence: "high",
|
|
355
|
+
evidence
|
|
356
|
+
};
|
|
357
|
+
if (hasUncertain) return {
|
|
358
|
+
recommendation: "uncertain",
|
|
359
|
+
confidence: "medium",
|
|
360
|
+
evidence
|
|
361
|
+
};
|
|
362
|
+
return {
|
|
363
|
+
recommendation: "ota",
|
|
364
|
+
confidence: hasJs ? "high" : "low",
|
|
365
|
+
evidence
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** pushy 官方测试包约定:与正式同源码、仅版本号偏移(major+1000,如 1.6.0→1001.6.0) */
|
|
369
|
+
const TEST_VERSION_OFFSET = 1e3;
|
|
370
|
+
/** 生成测试包偏移版本号(major+1000);非数字开头版本号无法偏移,显式报错 */
|
|
371
|
+
function testPackageVersion(versionName) {
|
|
372
|
+
const m = /^(\d+)(\..+)?$/.exec(versionName);
|
|
373
|
+
if (!m) throw new Error(`versionName「${versionName}」非 X.Y.Z 形态,无法按 major+${TEST_VERSION_OFFSET} 生成测试包偏移版本`);
|
|
374
|
+
return `${Number(m[1]) + TEST_VERSION_OFFSET}${m[2] ?? ""}`;
|
|
375
|
+
}
|
|
376
|
+
function platformAppId(opts, platform) {
|
|
377
|
+
const id = opts[platform];
|
|
378
|
+
if (typeof id !== "number") throw new Error(`平台 ${platform} 未配置热更 app 记录:rnUpdate({ ${platform}: <appId> })(在 applift.config.ts 的插件参数里声明)`);
|
|
379
|
+
return id;
|
|
380
|
+
}
|
|
381
|
+
/** 当前 versionName(readProjectVersions 按 framework 定位工程文件) */
|
|
382
|
+
function currentVersionName(ctx) {
|
|
383
|
+
const v = readProjectVersions(ctx.cwd, ctx.framework).find((x) => x.platform === ctx.platform);
|
|
384
|
+
if (!v?.versionName) throw new Error(`未读到平台 ${ctx.platform} 的 versionName(readProjectVersions 无该平台版本文件)`);
|
|
385
|
+
return v.versionName;
|
|
386
|
+
}
|
|
387
|
+
/** 服务端取号:该 versionName 下已有热更名 {ver}-patch.N 的最大 N → next = N+1(首版 N=0 → 1) */
|
|
388
|
+
function nextPatchNumber(names, versionName) {
|
|
389
|
+
const re = new RegExp(`^${versionName.replace(/\./g, "\\.")}-patch\\.(\\d+)$`);
|
|
390
|
+
let max = 0;
|
|
391
|
+
for (const { name } of names) {
|
|
392
|
+
const m = name.match(re);
|
|
393
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
394
|
+
}
|
|
395
|
+
return max + 1;
|
|
396
|
+
}
|
|
397
|
+
/** provider 构造(cli 注入点:测试 mock / 将来直连 API) */
|
|
398
|
+
function makeRnUpdateProvider(opts, cli = createPushyCli()) {
|
|
399
|
+
return {
|
|
400
|
+
id: "rn-update",
|
|
401
|
+
frameworks: ["rn"],
|
|
402
|
+
platforms: [
|
|
403
|
+
"harmony",
|
|
404
|
+
"android",
|
|
405
|
+
"ios"
|
|
406
|
+
].filter((k) => typeof opts[k] === "number"),
|
|
407
|
+
judge: (diff, ctx) => judgeDelivery(diff, ctx, opts),
|
|
408
|
+
async publishToTest(ctx) {
|
|
409
|
+
const appId = platformAppId(opts, ctx.platform);
|
|
410
|
+
const versionName = currentVersionName(ctx);
|
|
411
|
+
const name = `${versionName}-patch.${nextPatchNumber(await cli.versions(appId), versionName)}`;
|
|
412
|
+
const ppk = await cli.bundle(ctx.platform, ctx.cwd);
|
|
413
|
+
return {
|
|
414
|
+
versionName: name,
|
|
415
|
+
versionId: (await cli.publish({
|
|
416
|
+
appId,
|
|
417
|
+
ppk,
|
|
418
|
+
platform: ctx.platform,
|
|
419
|
+
name,
|
|
420
|
+
description: ctx.note,
|
|
421
|
+
packageVersion: testPackageVersion(versionName)
|
|
422
|
+
})).versionId
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
async promoteToProd(ctx) {
|
|
426
|
+
const appId = platformAppId(opts, ctx.platform);
|
|
427
|
+
const versionName = currentVersionName(ctx);
|
|
428
|
+
const existing = await cli.versions(appId);
|
|
429
|
+
const latest = existing.map((v) => {
|
|
430
|
+
const m = v.name.match(new RegExp(`^${versionName.replace(/\./g, "\\.")}-patch\\.(\\d+)$`));
|
|
431
|
+
return m ? {
|
|
432
|
+
...v,
|
|
433
|
+
n: Number(m[1])
|
|
434
|
+
} : null;
|
|
435
|
+
}).filter((x) => x !== null).sort((a, b) => b.n - a.n)[0];
|
|
436
|
+
if (!latest) {
|
|
437
|
+
const patchNames = existing.map((v) => v.name).filter((n) => /-patch\.\d+$/.test(n)).slice(0, 5);
|
|
438
|
+
throw new Error(`测试通道没有 ${versionName}-patch.N 热更版本可供 promote${patchNames.length > 0 ? `(现存:${patchNames.join(", ")}${existing.length > 5 ? " …" : ""})` : ""}:先 applift beta 发布测试热更并验证`);
|
|
439
|
+
}
|
|
440
|
+
const rollout = ctx.rollout ?? opts.defaultRollout;
|
|
441
|
+
await cli.rebind({
|
|
442
|
+
appId,
|
|
443
|
+
versionId: latest.id,
|
|
444
|
+
packageVersion: versionName,
|
|
445
|
+
rollout
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
versionName: latest.name,
|
|
449
|
+
...rollout !== void 0 ? { rollout } : {}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** 渠道 → 平台映射(热更已接入的平台;基线上传按此路由) */
|
|
455
|
+
const CHANNEL_PLATFORM = { agc: "harmony" };
|
|
456
|
+
/** rnUpdate 插件工厂:注册完整 provider(判定 + 测试发布 + promote)+ 原生发版自动基线。
|
|
457
|
+
* cli 注入点:测试 mock / 将来进程内直连 API。 */
|
|
458
|
+
function rnUpdate(opts = {}, cli = createPushyCli()) {
|
|
459
|
+
const provider = makeRnUpdateProvider(opts, cli);
|
|
460
|
+
return {
|
|
461
|
+
name: "rn-update",
|
|
462
|
+
"update:register"(providers) {
|
|
463
|
+
providers.push(provider);
|
|
464
|
+
},
|
|
465
|
+
async "stage:after"(result) {
|
|
466
|
+
const platform = CHANNEL_PLATFORM[result.channel];
|
|
467
|
+
if (!result.ok || !platform || !result.artifactPath) return;
|
|
468
|
+
const appId = opts[platform];
|
|
469
|
+
if (typeof appId !== "number") return;
|
|
470
|
+
try {
|
|
471
|
+
await cli.uploadApp(appId, result.artifactPath);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
process.stderr.write(`⚠ pushy 基线上传失败(请人工补传):${e.message}\n`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
export { createPushyCli, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, rnUpdate, spawnPushy, suggestFramework, tauriAdapter, tauriFramework };
|