@applift/core 0.8.0 → 0.8.1

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.
@@ -15,7 +15,11 @@ export declare function dingtalkNotify(options: DingtalkNotifyOptions): AppliftP
15
15
  * 见 rn-update.ts)——applift 不 spawn 构建命令(架构不变量 1)。
16
16
  */
17
17
  interface PushyCli {
18
- /** 发布热更版本(version/create + 绑定目标原生版本),返回版本 id */
18
+ /** 发布热更版本(version/create + 绑定目标原生版本),返回版本 id。
19
+ * deps 为服务端强校验所需(2026-09-17 后创建的应用要求 deps 里有 react-native-update
20
+ * >= 10.53.1——实测缺省被 400 拒绝),由调用方用 readDepVersions 从工程读取。
21
+ * 不上报 commit:实测带 commit(官方 getCommitInfo 同构形态)被 version/create 以
22
+ * 「上传凭据无效」拒绝(2026-10-21 实证,去 commit 即过);溯源走本地 history.jsonl */
19
23
  publish(input: {
20
24
  appId: number;
21
25
  ppk: string;
@@ -24,11 +28,12 @@ interface PushyCli {
24
28
  description?: string;
25
29
  packageVersion: string;
26
30
  rollout?: number;
31
+ deps: Record<string, string>;
27
32
  }): Promise<{
28
33
  versionId: number;
29
34
  }>;
30
- /** 上传原生基线(versionName 由调用方从产物读取后显式传入) */
31
- uploadApp(appId: number, appFile: string, versionName: string): Promise<{
35
+ /** 上传原生基线(versionName 由调用方从产物读取后显式传入;deps 同 publish) */
36
+ uploadApp(appId: number, appFile: string, versionName: string, deps: Record<string, string>): Promise<{
32
37
  packageId: number;
33
38
  version: string;
34
39
  }>;
@@ -58,7 +63,19 @@ interface PushyApiOptions {
58
63
  token: string;
59
64
  /** 注入点:测试 mock(缺省 globalThis.fetch) */
60
65
  fetchImpl?: FetchImpl;
66
+ /** 直传实现注入(缺省 node:http(s) 原生;测试 mock) */
67
+ httpUpload?: HttpUploadImpl;
61
68
  }
69
+ /** 直传实现(node:http(s) 原生;测试注入 mock) */
70
+ type HttpUploadImpl = (method: "POST" | "PUT", url: string, headers: Record<string, string>, body: Buffer) => Promise<{
71
+ status: number;
72
+ }>;
73
+ export declare function nodeHttpUpload(method: "POST" | "PUT", url: string, headers: Record<string, string>, body: Buffer): Promise<{
74
+ status: number;
75
+ }>;
76
+ /** 项目直接依赖(dependencies+devDependencies)的实装版本映射(逐级向上找 node_modules,键排序)。
77
+ * 对照官方 dep-versions.js 同构:服务端按 deps["react-native-update"] 做热更兼容门禁与变更提示 */
78
+ export declare function readDepVersions(cwd: string): Record<string, string>;
62
79
  /** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
63
80
  export declare function createPushyApi(opts: PushyApiOptions): PushyCli;
64
81
  //#endregion
@@ -90,4 +107,4 @@ export declare function makeRnUpdateProvider(opts: RnUpdateOptions, cli?: PushyC
90
107
  * cli 注入点:测试 mock / 特殊 transport;缺省 OpenAPI 客户端(token 来自 ctx.credentials)。 */
91
108
  export declare function rnUpdate(opts?: RnUpdateOptions, cli?: PushyCli): AppliftPlugin;
92
109
  //#endregion
93
- export { type DingtalkNotifyOptions, type FetchImpl, type PushyApiOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
110
+ export { type DingtalkNotifyOptions, type FetchImpl, type HttpUploadImpl, type PushyApiOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
@@ -3,6 +3,8 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHmac } from "node:crypto";
5
5
  import { spawnSync } from "node:child_process";
6
+ import nodeHttp from "node:http";
7
+ import nodeHttps from "node:https";
6
8
  //#region src/plugins/dingtalk-notify.ts
7
9
  /**
8
10
  * 官方示例插件 —— 钉钉机器人通知。
@@ -67,7 +69,7 @@ function dingtalkNotify(options) {
67
69
  const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
68
70
  /** pushy 官方 API 根(官方 CLI 默认端点簇的主端点) */
69
71
  const PUSHY_DEFAULT_BASE_URL = "https://update.reactnative.cn/api";
70
- const UA = "applift/pushy-openapi";
72
+ const UA = "react-native-update-cli/2.29.3";
71
73
  async function query(f, baseUrl, token, api, init) {
72
74
  let res;
73
75
  try {
@@ -94,7 +96,7 @@ async function query(f, baseUrl, token, api, init) {
94
96
  const msg = json?.message || res.statusText || `HTTP ${res.status}`;
95
97
  throw new Error(res.status === 401 ? `pushy API 鉴权失败(token 无效或过期):请更新 credentials.json 的 ${PUSHY_TOKEN_KEY}` : `pushy API ${api} 失败(HTTP ${res.status}):${msg}`);
96
98
  }
97
- return json;
99
+ return json ?? {};
98
100
  }
99
101
  /** 文件 part 的 Content-Type(对照官方 fileContentType 的扩展名表;未列出走 octet-stream) */
100
102
  function fileContentType(fileName) {
@@ -112,7 +114,7 @@ function fileContentType(fileName) {
112
114
  * (旧 CLI 流式是为进度条与代理改写,非容量需求)。不申请 chunked(响应分支少一半,
113
115
  * ppk 增量优化留待 diffStatus 端点,见设计说明)。
114
116
  */
115
- async function uploadFile(f, baseUrl, token, appId, file) {
117
+ async function uploadFile(f, baseUrl, token, appId, file, upload) {
116
118
  if (!fs.existsSync(file)) throw new Error(`pushy 上传文件不存在:${file}`);
117
119
  const size = fs.statSync(file).size;
118
120
  const instr = await query(f, baseUrl, token, "/upload", {
@@ -120,19 +122,18 @@ async function uploadFile(f, baseUrl, token, appId, file) {
120
122
  body: JSON.stringify({
121
123
  ext: path.extname(file),
122
124
  appId,
125
+ chunked: true,
123
126
  size
124
127
  })
125
128
  });
129
+ const httpUpload = upload ?? nodeHttpUpload;
126
130
  if (instr.method === "PUT") {
127
- const res = await f(instr.url, {
128
- method: "PUT",
129
- body: fs.readFileSync(file),
130
- headers: {
131
- "content-length": String(size),
132
- ...instr.headers ?? {}
133
- }
134
- });
135
- if (!res.ok) throw new Error(`pushy 基线直传失败(HTTP ${res.status}):${res.statusText || instr.url}`);
131
+ const putBody = fs.readFileSync(file);
132
+ const res = await httpUpload("PUT", instr.url, {
133
+ "content-length": String(size),
134
+ ...instr.headers ?? {}
135
+ }, putBody);
136
+ if (res.status > 299) throw new Error(`pushy 基线直传失败(HTTP ${res.status}):${instr.url}`);
136
137
  return instr.key;
137
138
  }
138
139
  const fields = Object.entries(instr.formData ?? {});
@@ -145,16 +146,32 @@ async function uploadFile(f, baseUrl, token, appId, file) {
145
146
  fs.readFileSync(file),
146
147
  Buffer.from(`\r\n--${boundary}--\r\n`)
147
148
  ]);
148
- const res = await f(instr.url, {
149
- method: "POST",
150
- body,
151
- headers: { "content-type": `multipart/form-data;boundary=${boundary}` }
152
- });
153
- if (!res.ok) throw new Error(`pushy 文件直传失败(HTTP ${res.status}):${res.statusText || instr.url}`);
149
+ const res = await httpUpload("POST", instr.url, {
150
+ accept: "*/*",
151
+ "user-agent": UA,
152
+ "content-type": `multipart/form-data;boundary=${boundary}`,
153
+ "content-length": String(body.length)
154
+ }, body);
155
+ if (res.status > 299) throw new Error(`pushy 文件直传失败(HTTP ${res.status}):${instr.url}`);
154
156
  const key = fields.find(([name]) => name === "key")?.[1];
155
157
  if (!key) throw new Error(`pushy /upload 指令缺 formData.key(服务端响应异常):${instr.url}`);
156
158
  return key;
157
159
  }
160
+ async function nodeHttpUpload(method, url, headers, body) {
161
+ const mod = url.startsWith("https:") ? nodeHttps : nodeHttp;
162
+ return new Promise((resolve, reject) => {
163
+ const req = mod.request(url, {
164
+ method,
165
+ headers
166
+ });
167
+ req.on("response", (res) => {
168
+ res.resume();
169
+ resolve({ status: res.statusCode ?? 0 });
170
+ });
171
+ req.on("error", (e) => reject(/* @__PURE__ */ new Error(`pushy 直传网络错误:${url}——${e.message}`)));
172
+ req.end(body);
173
+ });
174
+ }
158
175
  /** packageVersion → packageId 精确匹配(对照 CLI update:pkg.name === packageVersion) */
159
176
  async function bind(q, appId, versionId, packageVersion, rollout) {
160
177
  const pkgs = await listAllPackages(q, appId);
@@ -180,6 +197,31 @@ async function listAllPackages(q, appId) {
180
197
  }
181
198
  return out;
182
199
  }
200
+ /** 项目直接依赖(dependencies+devDependencies)的实装版本映射(逐级向上找 node_modules,键排序)。
201
+ * 对照官方 dep-versions.js 同构:服务端按 deps["react-native-update"] 做热更兼容门禁与变更提示 */
202
+ function readDepVersions(cwd) {
203
+ let pkg;
204
+ try {
205
+ pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
206
+ } catch {
207
+ return {};
208
+ }
209
+ const versions = {};
210
+ for (const dep of /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})])) {
211
+ let dir = path.resolve(cwd);
212
+ for (;;) {
213
+ try {
214
+ const installed = JSON.parse(fs.readFileSync(path.join(dir, "node_modules", dep, "package.json"), "utf8"));
215
+ if (typeof installed.version === "string") versions[dep] = installed.version;
216
+ break;
217
+ } catch {}
218
+ const parent = path.dirname(dir);
219
+ if (parent === dir) break;
220
+ dir = parent;
221
+ }
222
+ }
223
+ return Object.fromEntries(Object.keys(versions).sort().map((k) => [k, versions[k]]));
224
+ }
183
225
  /** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
184
226
  function createPushyApi(opts) {
185
227
  const baseUrl = opts.baseUrl ?? "https://update.reactnative.cn/api";
@@ -187,27 +229,29 @@ function createPushyApi(opts) {
187
229
  const q = (api, init) => query(f, baseUrl, opts.token, api, init);
188
230
  return {
189
231
  async publish(input) {
190
- const hash = await uploadFile(f, baseUrl, opts.token, input.appId, input.ppk);
232
+ const hash = await uploadFile(f, baseUrl, opts.token, input.appId, input.ppk, opts.httpUpload);
191
233
  const { id } = await q(`/app/${input.appId}/version/create`, {
192
234
  method: "POST",
193
235
  body: JSON.stringify({
194
236
  name: input.name,
195
237
  hash,
196
238
  description: input.description ?? "",
197
- metaInfo: ""
239
+ metaInfo: "",
240
+ deps: input.deps
198
241
  })
199
242
  });
200
243
  await bind(q, input.appId, id, input.packageVersion, input.rollout);
201
244
  return { versionId: id };
202
245
  },
203
- async uploadApp(appId, appFile, versionName) {
204
- const hash = await uploadFile(f, baseUrl, opts.token, appId, appFile);
246
+ async uploadApp(appId, appFile, versionName, deps) {
247
+ const hash = await uploadFile(f, baseUrl, opts.token, appId, appFile, opts.httpUpload);
205
248
  const { id } = await q(`/app/${appId}/package/create`, {
206
249
  method: "POST",
207
250
  body: JSON.stringify({
208
251
  name: versionName,
209
252
  hash,
210
- buildTime: String(Date.now())
253
+ buildTime: String(Date.now()),
254
+ deps
211
255
  })
212
256
  });
213
257
  return {
@@ -560,7 +604,8 @@ function makeRnUpdateProvider(opts, cli) {
560
604
  platform: ctx.platform,
561
605
  name,
562
606
  description: ctx.note,
563
- packageVersion: betaPackageVersion(versionName)
607
+ packageVersion: betaPackageVersion(versionName),
608
+ deps: readDepVersions(ctx.cwd)
564
609
  })).versionId
565
610
  };
566
611
  },
@@ -576,7 +621,8 @@ function makeRnUpdateProvider(opts, cli) {
576
621
  name,
577
622
  description: ctx.note,
578
623
  packageVersion: versionName,
579
- rollout
624
+ rollout,
625
+ deps: readDepVersions(ctx.cwd)
580
626
  })).versionId,
581
627
  ...rollout !== void 0 ? { rollout } : {}
582
628
  };
@@ -626,7 +672,7 @@ function rnUpdate(opts = {}, cli) {
626
672
  try {
627
673
  const versionName = readArtifactVersion(result.artifactPath)?.versionName;
628
674
  if (!versionName) throw new Error(`未能从产物读出 versionName:${result.artifactPath}(readArtifactVersion 不支持该格式?)`);
629
- await resolvePushyClient(opts, cli, hookCtx.credentials).uploadApp(appId, result.artifactPath, versionName);
675
+ await resolvePushyClient(opts, cli, hookCtx.credentials).uploadApp(appId, result.artifactPath, versionName, readDepVersions(hookCtx.cwd));
630
676
  } catch (e) {
631
677
  process.stderr.write(`⚠ pushy 基线上传失败(请人工补传):${e.message}\n`);
632
678
  }
@@ -634,4 +680,4 @@ function rnUpdate(opts = {}, cli) {
634
680
  };
635
681
  }
636
682
  //#endregion
637
- export { PUSHY_DEFAULT_BASE_URL, PUSHY_TOKEN_KEY, createPushyApi, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, rnUpdate, suggestFramework, tauriAdapter, tauriFramework };
683
+ export { PUSHY_DEFAULT_BASE_URL, PUSHY_TOKEN_KEY, createPushyApi, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, nodeHttpUpload, readDepVersions, rnAdapter, rnFramework, rnUpdate, suggestFramework, tauriAdapter, tauriFramework };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applift/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/hcg1023/applift.git",