Skip to content

Commit af3fffa

Browse files
luoqingmingclaude
andcommitted
feat: 审计第二轮 — Hermes 检测、按需加载命令模块、Node 18 冒烟、错误输出
- bundle-runner: iOS 不再只看 ios/Pods/hermes-engine,未 pod install 时兼看 Podfile.lock 与 Expo 的 Podfile.properties.json;Android 在 gradle.properties 与 build.gradle 都未提及时按 RN >= 0.71 默认 Hermes;gradle.properties 缺失 不再打印 ENOENT 堆栈;compileHermesByteCode 的 hermesc 惰性解析,复用 base 选择阶段已解析的路径 - bin/commands: 命令模块按命令名 require(新增 src/commands.ts),`help` 启动加载模块 129 -> 83、约 125ms -> 108ms;`--version` 别名;`-v` 只在首参 生效;参数解析纳入错误处理,未知命令/选项不再以未处理 rejection 崩溃; 错误默认只打印 message,RNU_DEBUG=1 时打印堆栈 - utils/git: 提交信息由 6 个 git 子进程合并为 3 个(NUL 分隔的单条 git log) - i18n: RNU_LANG=en|zh 覆盖品牌默认语言 - package.json: 发布包不再包含 src(exports map 已禁止子路径引用,且无 declarationMap);新增 `smoke` 脚本 - ci: 新增 node18-smoke job,在 engines 下限的 Node 18.17 上 require 构建产物 的全部模块并运行 help/-v,任何 Deprecation/Experimental 警告都视为失败 - tests: commands 注册表与 cli.json 双向一致性;Hermes 检测各分支; resolveLanguage Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tusm6iL2itjJZDiemujAeL
1 parent 4f6ea16 commit af3fffa

16 files changed

Lines changed: 514 additions & 96 deletions

File tree

.github/workflows/test.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ jobs:
6868
path: coverage
6969
if-no-files-found: error
7070

71+
node18-smoke:
72+
runs-on: blacksmith-4vcpu-ubuntu-2404
73+
timeout-minutes: 10
74+
75+
steps:
76+
- uses: actions/checkout@v7
77+
78+
- uses: oven-sh/setup-bun@v2
79+
80+
- name: Install Dependency
81+
run: bun install --frozen-lockfile
82+
83+
- name: Build package
84+
run: bun run build
85+
86+
# Node 18 goes on PATH only after the build: typescript >= 7 ships an
87+
# extensionless ESM bin/tsc that Node 18.17 cannot load.
88+
- name: Set up the oldest supported Node.js
89+
uses: actions/setup-node@v7
90+
with:
91+
node-version: '18.17.0'
92+
93+
- name: Load every built module and run the offline commands
94+
run: node scripts/smoke-lib.js
95+
7196
publish-dry-run:
7297
runs-on: blacksmith-4vcpu-ubuntu-2404
7398
timeout-minutes: 10

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ interface CLIProvider {
161161
```bash
162162
export PUSHY_REGISTRY=https://your-api-endpoint.com
163163
export NO_INTERACTIVE=true
164+
export RNU_LANG=en # CLI language (default: zh for pushy, en for cresc)
165+
export RNU_DEBUG=1 # print stack traces for errors
164166
```
165167

166168
## Sentry Sourcemaps

README.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ interface CLIProvider {
152152
```bash
153153
export PUSHY_REGISTRY=https://your-api-endpoint.com
154154
export NO_INTERACTIVE=true
155+
export RNU_LANG=en # 界面语言(默认:pushy 为 zh,cresc 为 en)
156+
export RNU_DEBUG=1 # 出错时打印完整堆栈
155157
```
156158

157159
## Sentry Sourcemap

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
},
2828
"files": [
2929
"lib",
30-
"src",
3130
"proto",
3231
"cli.json"
3332
],
@@ -39,6 +38,7 @@
3938
"lint": "bun run typecheck && biome check .",
4039
"lint:fix": "bun run typecheck && biome check --write .",
4140
"test": "bun test",
41+
"smoke": "node scripts/smoke-lib.js",
4242
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov",
4343
"benchmark:diff-stream": "bun scripts/benchmark-diff-stream.ts"
4444
},

scripts/smoke-lib.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env node
2+
// Loads every module of the built CLI and runs its offline commands on the
3+
// current Node.js. CI runs this on the oldest supported Node (see engines), so
4+
// a dependency or an API that needs a newer runtime fails here rather than on
5+
// a user's machine; the test suite itself runs under bun and cannot tell.
6+
7+
const { spawnSync } = require('node:child_process');
8+
const fs = require('node:fs');
9+
const path = require('node:path');
10+
11+
const lib = path.resolve(__dirname, '..', 'lib');
12+
if (!fs.existsSync(lib)) {
13+
console.error('lib/ not found: run `bun run build` first');
14+
process.exit(1);
15+
}
16+
17+
// these two run the CLI when required
18+
const entryPoints = new Set(['bin.js', 'bin-cresc.js']);
19+
20+
function listJs(dir) {
21+
const files = [];
22+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
23+
const full = path.join(dir, entry.name);
24+
if (entry.isDirectory()) {
25+
files.push(...listJs(full));
26+
} else if (entry.name.endsWith('.js')) {
27+
files.push(full);
28+
}
29+
}
30+
return files.sort();
31+
}
32+
33+
let loaded = 0;
34+
for (const file of listJs(lib)) {
35+
if (entryPoints.has(path.relative(lib, file))) continue;
36+
require(file);
37+
loaded += 1;
38+
}
39+
console.log(
40+
`smoke: loaded ${loaded} modules from lib/ on node ${process.version}`,
41+
);
42+
43+
function runCli(args) {
44+
const result = spawnSync(
45+
process.execPath,
46+
[path.join(lib, 'bin.js'), ...args],
47+
{
48+
encoding: 'utf8',
49+
env: { ...process.env, NO_INTERACTIVE: 'true', RNU_AUTO_UPDATE: '0' },
50+
},
51+
);
52+
const command = `pushy ${args.join(' ')}`;
53+
if (result.status !== 0) {
54+
console.error(result.stdout);
55+
console.error(result.stderr);
56+
throw new Error(`${command} exited with ${result.status}`);
57+
}
58+
// a deprecation or experimental warning on every command is a bug (it was
59+
// the punycode DEP0040 warning from node-fetch until 2.24)
60+
if (/(Deprecation|Experimental)Warning/.test(result.stderr)) {
61+
throw new Error(`${command} printed a runtime warning:\n${result.stderr}`);
62+
}
63+
return result;
64+
}
65+
66+
runCli(['help']);
67+
runCli(['-v']);
68+
console.log('smoke: `help` and `-v` ran without warnings');

src/bin.ts

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
#!/usr/bin/env node
22

33
import { loadSession } from './api';
4-
import { getAppCommands } from './app';
5-
import { bundleCommands } from './bundle';
6-
import { cacheCommands } from './cache';
7-
import { diffCommands } from './diff';
8-
import { installCommands } from './install';
9-
import { packageCommands } from './package';
10-
import { symbolicateCommands } from './symbolicate';
11-
import { userCommands } from './user';
4+
import { commandNames, loadCommandHandler } from './commands';
125
import { printVersionCommand } from './utils';
136
import { t } from './utils/i18n';
14-
import { versionCommands } from './versions';
15-
16-
type CliCommandHandler = (argv: any) => Promise<unknown> | unknown;
177

188
interface CliArgv {
199
command: string;
@@ -29,7 +19,7 @@ function printUsage(exitCode = 1) {
2919
console.log('React Native Update CLI');
3020
console.log('');
3121
console.log('Commands:');
32-
for (const name of Object.keys(commandHandlers)) {
22+
for (const name of commandNames) {
3323
console.log(` ${name}`);
3424
}
3525

@@ -45,22 +35,24 @@ function printUsage(exitCode = 1) {
4535
process.exit(exitCode);
4636
}
4737

48-
const commandHandlers: Record<string, CliCommandHandler> = {
49-
...userCommands,
50-
...bundleCommands,
51-
...cacheCommands,
52-
...diffCommands,
53-
...getAppCommands(),
54-
...packageCommands,
55-
...versionCommands,
56-
...symbolicateCommands,
57-
...installCommands,
58-
help: printUsage,
59-
};
38+
/**
39+
* Errors reaching the top level are almost always about the user's input,
40+
* project or network: print their message. The stack trace, which only helps
41+
* when the CLI itself is at fault, is printed on request (RNU_DEBUG=1).
42+
*/
43+
function reportError(err: unknown) {
44+
if (isTruthyEnv(process.env.RNU_DEBUG)) {
45+
console.error(err instanceof Error ? err.stack : err);
46+
return;
47+
}
48+
console.error(err instanceof Error ? err.message : String(err));
49+
console.error(t('errorStackHint'));
50+
}
6051

6152
async function run() {
62-
const versionOnly =
63-
process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version';
53+
const versionOnly = ['-v', '--version', 'version'].includes(
54+
process.argv[2] ?? '',
55+
);
6456
// The registry check for newer versions runs alongside the command (from a
6557
// 1-day cache when possible) and only ever delays `-v`/`version` itself; its
6658
// hint is printed once the command is done, or at exit for commands that
@@ -77,37 +69,39 @@ async function run() {
7769
}
7870
});
7971

80-
const argv: CliArgv = require('cli-arguments').parse(require('../cli.json'));
81-
global.NO_INTERACTIVE =
82-
Boolean(argv.options['no-interactive']) ||
83-
isTruthyEnv(process.env.NO_INTERACTIVE);
84-
global.USE_ACC_OSS =
85-
Boolean(argv.options.acc) || isTruthyEnv(process.env.USE_ACC_OSS);
86-
8772
try {
73+
// inside the try: an unknown command or option is reported like any other
74+
// error instead of crashing with an unhandled rejection
75+
const argv: CliArgv = require('cli-arguments').parse(
76+
require('../cli.json'),
77+
);
78+
global.NO_INTERACTIVE =
79+
Boolean(argv.options['no-interactive']) ||
80+
isTruthyEnv(process.env.NO_INTERACTIVE);
81+
global.USE_ACC_OSS =
82+
Boolean(argv.options.acc) || isTruthyEnv(process.env.USE_ACC_OSS);
83+
8884
await loadSession();
8985

90-
if (argv.command === 'help') {
86+
if (argv.command === 'help' || argv.command === 'list') {
9187
printUsage(0);
92-
} else if (argv.command === 'list') {
93-
printUsage(0);
94-
} else if (commandHandlers[argv.command]) {
95-
const handler = commandHandlers[argv.command];
96-
await handler(argv);
97-
// a check still in flight (cold cache) gets a short grace period; the
98-
// registry request itself is unref'd, so exiting never waits on it
99-
await versionCheck.settle(500);
100-
versionCheck.printHints();
101-
versionCheck.startAutoUpdate();
102-
} else {
88+
}
89+
const handler = loadCommandHandler(argv.command);
90+
if (!handler) {
10391
throw new Error(t('unknownCommand', { command: argv.command }));
10492
}
93+
await handler(argv);
94+
// a check still in flight (cold cache) gets a short grace period; the
95+
// registry request itself is unref'd, so exiting never waits on it
96+
await versionCheck.settle(500);
97+
versionCheck.printHints();
98+
versionCheck.startAutoUpdate();
10599
} catch (err: any) {
106-
if (err.status === 401) {
100+
if (err?.status === 401) {
107101
console.log(t('loginFirst'));
108102
process.exit(1);
109103
}
110-
console.error(err.stack);
104+
reportError(err);
111105
process.exit(1);
112106
}
113107
}

0 commit comments

Comments
 (0)