Skip to content

Commit bcf5417

Browse files
sunnylqmclaude
andcommitted
feat(publish): archive source maps with published versions; add symbolicate
- bundle: --sourcemap on by default (the composed packager+hermes map) - publish: uploads the map through /upload and reports sourceMapKey at version/create; warns when publishing without one, errors on a missing explicit path - symbolicate: pushy symbolicate <stack|-> --hash <updateHash> (or --versionId, --output) downloads the archived map via GET /app/{appId}/version/{id}/sourceMap and rewrites bundle frames, including Hermes "address at" frames (source-map 0.6.1) - provider API symbolicate(); Version.sourceMapKey; README (en/zh) Requires pushy-go/cresc-go with the 20260830-versions-source-map migration; older servers ignore sourceMapKey and lack the sourceMap route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tGFyF841jinkhw1933Zwp
1 parent 2a3777b commit bcf5417

15 files changed

Lines changed: 354 additions & 8 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ const publishResult = await provider.publish({
6868
- `hdiffFromApp`: Generate hdiff from APP files
6969
- `hdiffFromIpa`: Generate hdiff from IPA files
7070

71-
Hermes projects: `bundle` always runs hermesc with `-output-source-map`, so the debug info section is stripped from the bytecode (15–40% smaller, same as React Native's own release builds). The Hermes sourcemap stays in the intermediate directory (`.pushy/intermedia/<platform>/<bundle>.map`, never packed into the ppk); with `--sourcemap` it is composed with the packager map as before. Keep it if you want to symbolicate `address at` crash stacks later.
71+
Hermes projects: `bundle` always runs hermesc with `-output-source-map`, so the debug info section is stripped from the bytecode (15–40% smaller, same as React Native's own release builds). The Hermes sourcemap stays in the intermediate directory (`.pushy/intermedia/<platform>/<bundle>.map`, never packed into the ppk) and is composed with the packager map — `--sourcemap` is on by default since 2.23 (`--sourcemap false` opts out). When `bundle` publishes, that final map is uploaded and archived with the version (`sourceMapKey`), so `pushy symbolicate` can map crash stacks — including Hermes `address at` frames — back to source later. `pushy publish <ppk> --sourcemap <file.map>` archives a map for a ppk built elsewhere; publishing without a map prints a warning.
7272

7373
Hermes delta mode (`-base-bytecode`): by default (`--hermesBase auto`) `bundle` compiles against the previous HBC of the same app, which keeps Hermes string IDs stable and makes hot-update patches 5–30× smaller. The base comes from the server (`GET /app/:id/hermesBase`), verified by sha256 and kept in a local cache (`.pushy/cache/<sha256>`, 500 MB / 20 files, `PUSHY_CACHE_DIR` / `--cacheMaxMb` to tune, `pushy cache [clean]` to inspect or clear). `--hermesBase none` disables it; `--hermesBase <file.hbc|.ppk|.apk|.ipa>` uses a local artifact (for example the store build). `--verifyHermesBase` (default on) additionally compiles without the base (concurrently with the base compile) and compares both disassemblies; on any mismatch or failure the CLI silently falls back to the plain compile, so the feature can never block a release. Only hermesc builds that include the upstream delta-mode fix are used (classic `react-native/sdks/hermesc`, or `hermes-compiler` ≥ 250829098). If a base compile fails, the full hermesc output is written to `hermes-base-error.log` next to the intermediate directory. `--resetCache false` skips Metro's `--reset-cache` and reuses its transform cache, which makes repeated bundles much faster.
7474

7575
### Version
7676

77-
- `publish`: Publish a new version
77+
- `publish`: Publish a new version (`--sourcemap <file.map>` archives the source map with it)
78+
- `symbolicate`: Map a crash stack to source using the archived map: `pushy symbolicate stack.txt --hash <updateHash>` (`-` reads stdin, `--versionId` instead of `--hash`, `--output <file>`); the hash is `getUpdateMetadata().currentVersion` on the device
7879
- `versions`: List versions
7980
- `update`: Update version package rules
8081
- `updateVersionInfo`: Update version metadata

README.zh-CN.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ const publishResult = await provider.publish({
6868
- `hdiffFromApp`: 基于 APP 文件生成 hdiff
6969
- `hdiffFromIpa`: 基于 IPA 文件生成 hdiff
7070

71-
Hermes 工程:`bundle` 调用 hermesc 时始终带 `-output-source-map`,因此字节码不含 debug info 段(小 15%~40%,与 React Native 自身 release 构建一致)。Hermes sourcemap 保留在中间目录(`.pushy/intermedia/<platform>/<bundle>.map`,不会打进 ppk);传 `--sourcemap` 时照旧与 packager map 合成。留着它可以事后符号化 `address at` 形式的崩溃堆栈
71+
Hermes 工程:`bundle` 调用 hermesc 时始终带 `-output-source-map`,因此字节码不含 debug info 段(小 15%~40%,与 React Native 自身 release 构建一致)。Hermes sourcemap 保留在中间目录(`.pushy/intermedia/<platform>/<bundle>.map`,不会打进 ppk),并与 packager map 合成——自 2.23 起 `--sourcemap` 默认开启(`--sourcemap false` 关闭)。`bundle` 发布时会把这份最终 map 上传并随版本归档(`sourceMapKey`),之后用 `pushy symbolicate` 即可把崩溃堆栈(含 Hermes 的 `address at` 帧)还原到源码。别处打好的 ppk 可用 `pushy publish <ppk> --sourcemap <file.map>` 归档;不带 map 发布会打印警告
7272

7373
Hermes delta 模式(`-base-bytecode`):默认 `--hermesBase auto`,`bundle` 会以同一应用上一版的 HBC 为 base 编译,让 Hermes 字符串 ID 跨版本稳定,热更 patch 可缩小 5~30 倍。base 由服务端(`GET /app/:id/hermesBase`)给出、按 sha256 校验并存入本地缓存(`.pushy/cache/<sha256>`,默认 500 MB / 20 个,可用 `PUSHY_CACHE_DIR` / `--cacheMaxMb` 调整,`pushy cache [clean]` 查看或清空)。`--hermesBase none` 关闭;`--hermesBase <file.hbc|.ppk|.apk|.ipa>` 指定本地文件(比如商店包)作 base。`--verifyHermesBase`(默认开)会并行再做一次普通编译并比对两份反汇编;任何不一致或失败都静默回退到普通编译,不会阻塞发版。只有包含上游 delta 模式修复的 hermesc 才会启用(经典 `react-native/sdks/hermesc`,或 `hermes-compiler` ≥ 250829098)。base 编译失败时,完整的 hermesc 输出会写到中间目录旁边的 `hermes-base-error.log`。`--resetCache false` 可跳过 Metro 的 `--reset-cache`,复用其转换缓存,重复打包会快很多。
7474

7575
### Version
7676

77-
- `publish`: 发布新版本
77+
- `publish`: 发布新版本(`--sourcemap <file.map>` 随版本归档 source map)
78+
- `symbolicate`: 用归档的 map 还原崩溃堆栈:`pushy symbolicate stack.txt --hash <热更 hash>``-` 读 stdin,可用 `--versionId` 代替 `--hash``--output <文件>` 写文件);hash 即设备上 `getUpdateMetadata().currentVersion`
7879
- `versions`: 列出版本
7980
- `update`: 更新版本包规则
8081
- `updateVersionInfo`: 更新版本元信息

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@
139139
},
140140
"dryRun": {
141141
"default": false
142+
},
143+
"sourcemap": {
144+
"hasValue": true
142145
}
143146
}
144147
},
@@ -248,7 +251,7 @@
248251
"hasValue": true
249252
},
250253
"sourcemap": {
251-
"default": false
254+
"default": true
252255
},
253256
"taro": {
254257
"default": false
@@ -382,6 +385,23 @@
382385
"cache": {
383386
"description": "Inspect or clean the local bundle cache: cache [clean]",
384387
"options": {}
388+
},
389+
"symbolicate": {
390+
"description": "Map a JS stack trace back to source using the source map archived with a published version.",
391+
"options": {
392+
"hash": {
393+
"hasValue": true
394+
},
395+
"versionId": {
396+
"hasValue": true
397+
},
398+
"platform": {
399+
"hasValue": true
400+
},
401+
"output": {
402+
"hasValue": true
403+
}
404+
}
385405
}
386406
},
387407
"globalOptions": {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"protobufjs": "^8.7.2",
8080
"read": "^4.1.0",
8181
"registry-auth-token": "^5.1.1",
82+
"source-map": "0.6.1",
8283
"tty-table": "5.0",
8384
"yauzl": "^3.4.0",
8485
"yazl": "3.3.1"

src/bin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { cacheCommands } from './cache';
77
import { diffCommands } from './diff';
88
import { installCommands } from './install';
99
import { packageCommands } from './package';
10+
import { symbolicateCommands } from './symbolicate';
1011
import { userCommands } from './user';
1112
import { printVersionCommand } from './utils';
1213
import { t } from './utils/i18n';
@@ -52,6 +53,7 @@ const commandHandlers: Record<string, CliCommandHandler> = {
5253
...getAppCommands(),
5354
...packageCommands,
5455
...versionCommands,
56+
...symbolicateCommands,
5557
...installCommands,
5658
help: printUsage,
5759
};

src/bundle.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type PublishBundlePayload = {
7272
rollout?: string;
7373
dryRun?: boolean;
7474
hermesBase?: HermesBaseMeta;
75+
/** path of the final source map to archive with the version */
76+
sourcemap?: string;
7577
};
7678

7779
function getAliasedOptionalStringOption(
@@ -108,7 +110,9 @@ export function normalizeBundleOptions(
108110
`${tempDir}/output/${platform}.\${time}.ppk`,
109111
),
110112
dev: getBooleanOption(translatedOptions, 'dev', false) ? 'true' : 'false',
111-
sourcemap: getBooleanOption(translatedOptions, 'sourcemap', false),
113+
// On by default since 2.23: the map is archived with the published
114+
// version (pushy symbolicate). --no-sourcemap / --sourcemap=false opts out.
115+
sourcemap: getBooleanOption(translatedOptions, 'sourcemap', true),
112116
taro: getBooleanOption(translatedOptions, 'taro', false),
113117
expo: getBooleanOption(translatedOptions, 'expo', false),
114118
rncli: getBooleanOption(translatedOptions, 'rncli', false),
@@ -295,6 +299,10 @@ export const bundleCommands = {
295299
rollout: normalized.rollout,
296300
dryRun: normalized.dryRun,
297301
hermesBase: baseMeta,
302+
sourcemap:
303+
normalized.sourcemap || bundleParams.sourcemap
304+
? sourcemapOutput
305+
: undefined,
298306
});
299307
await uploadSentryArtifactsIfNeeded(
300308
bundleParams.sentry,
@@ -315,6 +323,10 @@ export const bundleCommands = {
315323
if (v.toLowerCase() === 'y') {
316324
await publishBundleVersion(realOutput, platform, {
317325
hermesBase: baseMeta,
326+
sourcemap:
327+
normalized.sourcemap || bundleParams.sourcemap
328+
? sourcemapOutput
329+
: undefined,
318330
});
319331
await uploadSentryArtifactsIfNeeded(
320332
bundleParams.sentry,

src/locales/en.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,18 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
130130
processingStringPool: 'Processing the string pool ...',
131131
publishUsage:
132132
'Usage: pushy publish <ppk file> --platform ios|android|harmony',
133+
sourceMapNotFound: 'Source map not found: {{path}}',
134+
sourceMapMissingWarning:
135+
'No source map was archived with this version: crashes cannot be symbolicated later. Bundle with the default --sourcemap, or pass --sourcemap <file> to publish.',
136+
sourceMapArchived: 'Source map archived with version {{id}}',
137+
symbolicateUsage:
138+
'Usage: pushy symbolicate <stack file | -> --hash <updateHash> [--platform ios|android|harmony] [--output <file>]',
139+
symbolicateVersionNotFound:
140+
'No published version with hash {{hash}} found for this app',
141+
symbolicateNoSourceMap:
142+
'Version {{id}} was published without a source map; nothing to symbolicate against',
143+
symbolicateDone:
144+
'Symbolicated {{count}} frame(s) against version {{id}} ({{hash}})',
133145
rnuVersionNotFound:
134146
'react-native-update: Cannot get the version number. Please run the command in the project directory',
135147
rolloutConfigSet:

src/locales/zh.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ export default {
118118
processingError: '处理文件时出错:{{error}}',
119119
processingPackage: '正在处理包 {{count}}...',
120120
processingStringPool: '正在处理字符串池...',
121+
sourceMapNotFound: '找不到 source map:{{path}}',
122+
sourceMapMissingWarning:
123+
'本版本没有归档 source map,之后的崩溃堆栈将无法还原。请用默认的 --sourcemap 打包,或在 publish 时传 --sourcemap <文件>。',
124+
sourceMapArchived: 'source map 已随版本 {{id}} 归档',
125+
symbolicateUsage:
126+
'用法:pushy symbolicate <堆栈文件 | -> --hash <热更 hash> [--platform ios|android|harmony] [--output <文件>]',
127+
symbolicateVersionNotFound: '当前应用下找不到 hash 为 {{hash}} 的已发布版本',
128+
symbolicateNoSourceMap: '版本 {{id}} 发布时未归档 source map,无法还原',
129+
symbolicateDone: '已按版本 {{id}}({{hash}})还原 {{count}} 个堆栈帧',
121130
publishUsage:
122131
'使用方法: pushy publish ppk后缀文件 --platform ios|android|harmony',
123132
rnuVersionNotFound:

src/provider.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Platform,
99
PublishOptions,
1010
Session,
11+
SymbolicateOptions,
1112
UpdateVersionOptions,
1213
UploadOptions,
1314
} from './types';
@@ -128,6 +129,7 @@ export class CLIProviderImpl implements CLIProvider {
128129
? undefined
129130
: String(options.rollout),
130131
dryRun: options.dryRun || false,
132+
sourcemap: options.sourcemap,
131133
},
132134
options.filePath ? [options.filePath] : [],
133135
);
@@ -140,6 +142,28 @@ export class CLIProviderImpl implements CLIProvider {
140142
);
141143
}
142144

145+
async symbolicate(options: SymbolicateOptions): Promise<CommandResult> {
146+
return this.runMessageCommand(
147+
async () => {
148+
const context = this.createContext(
149+
{
150+
hash: options.hash,
151+
versionId: options.versionId,
152+
platform: options.platform,
153+
appId: options.appId,
154+
config: options.config,
155+
output: options.output,
156+
},
157+
options.stackFile ? [options.stackFile] : [],
158+
);
159+
const { symbolicateCommands } = await import('./symbolicate');
160+
await symbolicateCommands.symbolicate(context);
161+
},
162+
'Unknown error during symbolication',
163+
'Stack symbolicated',
164+
);
165+
}
166+
143167
async upload(options: UploadOptions): Promise<CommandResult> {
144168
return this.runMessageCommand(
145169
async () => {

0 commit comments

Comments
 (0)