rn-update-cli 1.0.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/LICENSE +21 -0
- package/README.md +167 -0
- package/README.zh-CN.md +167 -0
- package/cli.json +525 -0
- package/lib/api.d.ts +56 -0
- package/lib/api.js +671 -0
- package/lib/app.d.ts +107 -0
- package/lib/app.js +329 -0
- package/lib/auto-update.d.ts +35 -0
- package/lib/auto-update.js +507 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +133 -0
- package/lib/bundle-pack.d.ts +6 -0
- package/lib/bundle-pack.js +91 -0
- package/lib/bundle-runner.d.ts +168 -0
- package/lib/bundle-runner.js +1305 -0
- package/lib/bundle.d.ts +45 -0
- package/lib/bundle.js +226 -0
- package/lib/cache.d.ts +10 -0
- package/lib/cache.js +25 -0
- package/lib/commands.d.ts +11 -0
- package/lib/commands.js +72 -0
- package/lib/diff.d.ts +18 -0
- package/lib/diff.js +690 -0
- package/lib/exports.d.ts +11 -0
- package/lib/exports.js +20 -0
- package/lib/install.d.ts +9 -0
- package/lib/install.js +51 -0
- package/lib/locales/en.d.ts +187 -0
- package/lib/locales/en.js +194 -0
- package/lib/locales/zh.d.ts +187 -0
- package/lib/locales/zh.js +194 -0
- package/lib/native-package.d.ts +21 -0
- package/lib/native-package.js +298 -0
- package/lib/package.d.ts +68 -0
- package/lib/package.js +487 -0
- package/lib/provider.d.ts +24 -0
- package/lib/provider.js +244 -0
- package/lib/standalone-publish.d.ts +17 -0
- package/lib/standalone-publish.js +433 -0
- package/lib/symbolicate.d.ts +35 -0
- package/lib/symbolicate.js +130 -0
- package/lib/types.d.ts +152 -0
- package/lib/types.js +2 -0
- package/lib/user.d.ts +8 -0
- package/lib/user.js +50 -0
- package/lib/utils/add-gitignore.d.ts +6 -0
- package/lib/utils/add-gitignore.js +44 -0
- package/lib/utils/app-info-parser/aab.d.ts +27 -0
- package/lib/utils/app-info-parser/aab.js +256 -0
- package/lib/utils/app-info-parser/apk.d.ts +19 -0
- package/lib/utils/app-info-parser/apk.js +95 -0
- package/lib/utils/app-info-parser/app.d.ts +9 -0
- package/lib/utils/app-info-parser/app.js +15 -0
- package/lib/utils/app-info-parser/index.d.ts +21 -0
- package/lib/utils/app-info-parser/index.js +51 -0
- package/lib/utils/app-info-parser/ipa.d.ts +19 -0
- package/lib/utils/app-info-parser/ipa.js +101 -0
- package/lib/utils/app-info-parser/resource-finder.d.ts +49 -0
- package/lib/utils/app-info-parser/resource-finder.js +429 -0
- package/lib/utils/app-info-parser/utils.d.ts +36 -0
- package/lib/utils/app-info-parser/utils.js +140 -0
- package/lib/utils/app-info-parser/xml-parser/binary.d.ts +61 -0
- package/lib/utils/app-info-parser/xml-parser/binary.js +577 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.d.ts +15 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.js +207 -0
- package/lib/utils/app-info-parser/zip.d.ts +29 -0
- package/lib/utils/app-info-parser/zip.js +97 -0
- package/lib/utils/check-lockfile.d.ts +6 -0
- package/lib/utils/check-lockfile.js +93 -0
- package/lib/utils/check-plugin.d.ts +12 -0
- package/lib/utils/check-plugin.js +36 -0
- package/lib/utils/command-result.d.ts +8 -0
- package/lib/utils/command-result.js +27 -0
- package/lib/utils/constants.d.ts +10 -0
- package/lib/utils/constants.js +39 -0
- package/lib/utils/dep-versions.d.ts +20 -0
- package/lib/utils/dep-versions.js +129 -0
- package/lib/utils/git.d.ts +14 -0
- package/lib/utils/git.js +64 -0
- package/lib/utils/hbcTransform.d.ts +111 -0
- package/lib/utils/hbcTransform.js +561 -0
- package/lib/utils/hermes-base.d.ts +227 -0
- package/lib/utils/hermes-base.js +1287 -0
- package/lib/utils/hermes-literals.d.ts +64 -0
- package/lib/utils/hermes-literals.js +260 -0
- package/lib/utils/http-helper.d.ts +10 -0
- package/lib/utils/http-helper.js +150 -0
- package/lib/utils/i18n.d.ts +22 -0
- package/lib/utils/i18n.js +46 -0
- package/lib/utils/index.d.ts +67 -0
- package/lib/utils/index.js +545 -0
- package/lib/utils/latest-version/index.d.ts +154 -0
- package/lib/utils/latest-version/index.js +331 -0
- package/lib/utils/options.d.ts +10 -0
- package/lib/utils/options.js +68 -0
- package/lib/utils/plugin-config.d.ts +14 -0
- package/lib/utils/plugin-config.js +66 -0
- package/lib/utils/runtime.d.ts +54 -0
- package/lib/utils/runtime.js +263 -0
- package/lib/utils/slim-sourcemap.d.ts +28 -0
- package/lib/utils/slim-sourcemap.js +106 -0
- package/lib/utils/zip-entries.d.ts +10 -0
- package/lib/utils/zip-entries.js +211 -0
- package/lib/utils/zip-options.d.ts +17 -0
- package/lib/utils/zip-options.js +190 -0
- package/lib/utils/zip-range.d.ts +148 -0
- package/lib/utils/zip-range.js +581 -0
- package/lib/utils/zip-raw-writer.d.ts +52 -0
- package/lib/utils/zip-raw-writer.js +209 -0
- package/lib/version-operations.d.ts +21 -0
- package/lib/version-operations.js +320 -0
- package/lib/versions.d.ts +66 -0
- package/lib/versions.js +402 -0
- package/package.json +113 -0
- package/proto/Configuration.proto +183 -0
- package/proto/Resources.proto +569 -0
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.hasProjectDependency = hasProjectDependency;
|
|
40
|
+
exports.resolveExpoCli = resolveExpoCli;
|
|
41
|
+
exports.runReactNativeBundleCommand = runReactNativeBundleCommand;
|
|
42
|
+
exports.assertSafeToEmpty = assertSafeToEmpty;
|
|
43
|
+
exports.detectHermesEnabled = detectHermesEnabled;
|
|
44
|
+
exports.detectIosHermes = detectIosHermes;
|
|
45
|
+
exports.resolveHermesCommand = resolveHermesCommand;
|
|
46
|
+
exports.summarizeHermescStderr = summarizeHermescStderr;
|
|
47
|
+
exports.buildHermescArgs = buildHermescArgs;
|
|
48
|
+
exports.compileHermesByteCode = compileHermesByteCode;
|
|
49
|
+
exports.hermesBaseErrorLogPath = hermesBaseErrorLogPath;
|
|
50
|
+
exports.hermesBaseDumpPaths = hermesBaseDumpPaths;
|
|
51
|
+
exports.startHermesBaseSelection = startHermesBaseSelection;
|
|
52
|
+
exports.copyDebugidForSentry = copyDebugidForSentry;
|
|
53
|
+
exports.prepareSentryUploadArtifacts = prepareSentryUploadArtifacts;
|
|
54
|
+
exports.readSourcemapDebugId = readSourcemapDebugId;
|
|
55
|
+
exports.resolveSentryUploadMode = resolveSentryUploadMode;
|
|
56
|
+
exports.buildSentrySourcemapsUploadArgs = buildSentrySourcemapsUploadArgs;
|
|
57
|
+
exports.uploadSourcemapForSentry = uploadSourcemapForSentry;
|
|
58
|
+
/**
|
|
59
|
+
* [INPUT]: 依赖 Node 子进程、React Native bundle、Hermes 编译器与安全临时目录校验
|
|
60
|
+
* [OUTPUT]: 对外提供 bundle 执行、Hermes base 编译与等价结果、Sourcemap 上传参数和中间目录安全断言
|
|
61
|
+
* [POS]: CLI 原生打包编排层,连接 Metro/Hermes 与 ppk 制品生成,不负责 HTTP 发布
|
|
62
|
+
*/
|
|
63
|
+
const child_process_1 = require("child_process");
|
|
64
|
+
const compare_versions_1 = require("compare-versions");
|
|
65
|
+
const fs = __importStar(require("fs-extra"));
|
|
66
|
+
const os_1 = __importDefault(require("os"));
|
|
67
|
+
const path_1 = __importDefault(require("path"));
|
|
68
|
+
const api_1 = require("./api");
|
|
69
|
+
const dep_versions_1 = require("./utils/dep-versions");
|
|
70
|
+
const hermes_base_1 = require("./utils/hermes-base");
|
|
71
|
+
const i18n_1 = require("./utils/i18n");
|
|
72
|
+
const runtime_1 = require("./utils/runtime");
|
|
73
|
+
const g2js = require('gradle-to-js/lib/parser');
|
|
74
|
+
const properties = require('properties');
|
|
75
|
+
const dependencyFields = [
|
|
76
|
+
'dependencies',
|
|
77
|
+
'devDependencies',
|
|
78
|
+
'peerDependencies',
|
|
79
|
+
'optionalDependencies',
|
|
80
|
+
];
|
|
81
|
+
const ANDROID_SENTRY_BUNDLE_NAME = 'index.android.bundle';
|
|
82
|
+
function hasProjectDependency(dependencyName, projectRoot = process.cwd()) {
|
|
83
|
+
try {
|
|
84
|
+
const packageJson = JSON.parse(fs.readFileSync(path_1.default.join(projectRoot, 'package.json'), 'utf8'));
|
|
85
|
+
return dependencyFields.some((field) => {
|
|
86
|
+
const dependencies = packageJson[field];
|
|
87
|
+
if (typeof dependencies !== 'object' || dependencies === null) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return dependencyName in dependencies;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function resolveExpoCli(projectRoot = process.cwd()) {
|
|
98
|
+
if (!hasProjectDependency('expo', projectRoot)) {
|
|
99
|
+
return {
|
|
100
|
+
cliPath: '',
|
|
101
|
+
usingExpo: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const searchPaths = [projectRoot];
|
|
106
|
+
try {
|
|
107
|
+
const expoPackageJsonPath = require.resolve('expo/package.json', {
|
|
108
|
+
paths: [projectRoot],
|
|
109
|
+
});
|
|
110
|
+
searchPaths.push(path_1.default.dirname(expoPackageJsonPath));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// expo 包不存在,忽略
|
|
114
|
+
}
|
|
115
|
+
const cliPath = require.resolve('@expo/cli', {
|
|
116
|
+
paths: searchPaths,
|
|
117
|
+
});
|
|
118
|
+
const expoCliVersion = JSON.parse(fs.readFileSync(require.resolve('@expo/cli/package.json', {
|
|
119
|
+
paths: searchPaths,
|
|
120
|
+
}), 'utf8')).version;
|
|
121
|
+
if (!(0, compare_versions_1.satisfies)(expoCliVersion, '>= 0.10.17')) {
|
|
122
|
+
return {
|
|
123
|
+
cliPath: '',
|
|
124
|
+
usingExpo: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
cliPath,
|
|
129
|
+
usingExpo: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return {
|
|
134
|
+
cliPath: '',
|
|
135
|
+
usingExpo: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function runReactNativeBundleCommand({ bundleName, dev, entryFile, outputFolder, platform, sourcemapOutput, config, forceHermes, hermesBase, resetCache = true, cli, isSentry, }) {
|
|
140
|
+
let gradleConfig = {};
|
|
141
|
+
if (platform === 'android') {
|
|
142
|
+
gradleConfig = await checkGradleConfig(process.cwd());
|
|
143
|
+
if (gradleConfig.crunchPngs !== false) {
|
|
144
|
+
console.warn((0, i18n_1.t)('androidCrunchPngsWarning'));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const reactNativeBundleArgs = [];
|
|
148
|
+
const envArgs = process.env.PAKTA_ENV_ARGS;
|
|
149
|
+
if (envArgs) {
|
|
150
|
+
reactNativeBundleArgs.push(...envArgs.trim().split(/\s+/));
|
|
151
|
+
}
|
|
152
|
+
outputFolder = assertSafeToEmpty(outputFolder);
|
|
153
|
+
fs.emptyDirSync(outputFolder);
|
|
154
|
+
let cliPath = '';
|
|
155
|
+
let usingExpo = false;
|
|
156
|
+
const getExpoCli = () => {
|
|
157
|
+
const resolvedExpoCli = resolveExpoCli();
|
|
158
|
+
cliPath = resolvedExpoCli.cliPath;
|
|
159
|
+
usingExpo = resolvedExpoCli.usingExpo;
|
|
160
|
+
};
|
|
161
|
+
const getRnCli = () => {
|
|
162
|
+
try {
|
|
163
|
+
cliPath = require.resolve('react-native/local-cli/cli.js', {
|
|
164
|
+
paths: [process.cwd()],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
cliPath = require.resolve('@react-native-community/cli/build/bin.js', {
|
|
169
|
+
paths: [process.cwd()],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const getTaroCli = () => {
|
|
174
|
+
try {
|
|
175
|
+
cliPath = require.resolve('@tarojs/cli/bin/taro', {
|
|
176
|
+
paths: [process.cwd()],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// fallback 到 RN CLI
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
if (cli.expo) {
|
|
184
|
+
getExpoCli();
|
|
185
|
+
}
|
|
186
|
+
else if (cli.taro) {
|
|
187
|
+
getTaroCli();
|
|
188
|
+
}
|
|
189
|
+
else if (cli.rncli) {
|
|
190
|
+
getRnCli();
|
|
191
|
+
}
|
|
192
|
+
if (!cliPath) {
|
|
193
|
+
getExpoCli();
|
|
194
|
+
if (!usingExpo) {
|
|
195
|
+
getRnCli();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isSentry) {
|
|
199
|
+
if (platform === 'ios') {
|
|
200
|
+
process.env.SENTRY_PROPERTIES = 'ios/sentry.properties';
|
|
201
|
+
}
|
|
202
|
+
else if (platform === 'android') {
|
|
203
|
+
process.env.SENTRY_PROPERTIES = 'android/sentry.properties';
|
|
204
|
+
}
|
|
205
|
+
else if (platform === 'harmony' &&
|
|
206
|
+
fs.existsSync('harmony/sentry.properties')) {
|
|
207
|
+
process.env.SENTRY_PROPERTIES = 'harmony/sentry.properties';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let bundleCommand = 'bundle';
|
|
211
|
+
if (usingExpo) {
|
|
212
|
+
bundleCommand = 'export:embed';
|
|
213
|
+
}
|
|
214
|
+
else if (platform === 'harmony') {
|
|
215
|
+
bundleCommand = 'bundle-harmony';
|
|
216
|
+
}
|
|
217
|
+
else if (cli.taro) {
|
|
218
|
+
bundleCommand = 'build';
|
|
219
|
+
}
|
|
220
|
+
if (platform === 'harmony') {
|
|
221
|
+
bundleName = 'bundle.harmony.js';
|
|
222
|
+
if (forceHermes === undefined) {
|
|
223
|
+
forceHermes = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
reactNativeBundleArgs.push(cliPath, bundleCommand, '--assets-dest', outputFolder, '--bundle-output', path_1.default.join(outputFolder, bundleName));
|
|
227
|
+
if (platform !== 'harmony') {
|
|
228
|
+
reactNativeBundleArgs.push('--platform', platform);
|
|
229
|
+
if (resetCache)
|
|
230
|
+
reactNativeBundleArgs.push('--reset-cache');
|
|
231
|
+
}
|
|
232
|
+
if (cli.taro) {
|
|
233
|
+
reactNativeBundleArgs.push('--type', 'rn');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
reactNativeBundleArgs.push('--dev', dev, '--entry-file', entryFile);
|
|
237
|
+
}
|
|
238
|
+
if (sourcemapOutput) {
|
|
239
|
+
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
|
|
240
|
+
}
|
|
241
|
+
if (config) {
|
|
242
|
+
reactNativeBundleArgs.push('--config', config);
|
|
243
|
+
}
|
|
244
|
+
// Decide about Hermes before Metro runs so the base lookup (server query,
|
|
245
|
+
// Range download, cache) overlaps the bundling instead of following it.
|
|
246
|
+
const hermesEnabled = await detectHermesEnabled(platform, forceHermes, gradleConfig);
|
|
247
|
+
const jsRuntime = (0, runtime_1.getJavaScriptRuntime)();
|
|
248
|
+
const reactNativeBundleProcess = (0, runtime_1.spawnJavaScript)(reactNativeBundleArgs);
|
|
249
|
+
console.log(`Running bundle command: ${jsRuntime} ${reactNativeBundleArgs.join(' ')}`);
|
|
250
|
+
// Metro is running; the synchronous head of the selection (hermesc lookup,
|
|
251
|
+
// HBC version probe) now overlaps it instead of delaying its start.
|
|
252
|
+
const pendingBase = hermesEnabled && hermesBase
|
|
253
|
+
? startHermesBaseSelection(hermesBase)
|
|
254
|
+
: undefined;
|
|
255
|
+
let hermesResult = null;
|
|
256
|
+
await new Promise((resolve, reject) => {
|
|
257
|
+
reactNativeBundleProcess.stdout.on('data', (data) => {
|
|
258
|
+
console.log(data.toString().trim());
|
|
259
|
+
});
|
|
260
|
+
reactNativeBundleProcess.stderr.on('data', (data) => {
|
|
261
|
+
console.error(data.toString().trim());
|
|
262
|
+
});
|
|
263
|
+
reactNativeBundleProcess.once('error', reject);
|
|
264
|
+
reactNativeBundleProcess.on('close', async (exitCode) => {
|
|
265
|
+
if (exitCode) {
|
|
266
|
+
reject(new Error((0, i18n_1.t)('bundleCommandError', { code: exitCode })));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
if (hermesEnabled) {
|
|
271
|
+
hermesResult = await compileHermesByteCode({
|
|
272
|
+
bundleName,
|
|
273
|
+
outputFolder,
|
|
274
|
+
sourcemapOutput,
|
|
275
|
+
shouldCleanSourcemap: !isSentry,
|
|
276
|
+
baseRequest: hermesBase,
|
|
277
|
+
pendingBase,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (platform === 'harmony') {
|
|
281
|
+
const harmonyRawAssetsPath = 'harmony/entry/src/main/resources/rawfile/assets';
|
|
282
|
+
fs.ensureDirSync(harmonyRawAssetsPath);
|
|
283
|
+
fs.copySync(outputFolder, harmonyRawAssetsPath, {
|
|
284
|
+
overwrite: true,
|
|
285
|
+
// sourcemaps must not ship inside the native package
|
|
286
|
+
filter: (src) => !src.endsWith('.map'),
|
|
287
|
+
});
|
|
288
|
+
fs.moveSync(`${harmonyRawAssetsPath}/bundle.harmony.js`, `${harmonyRawAssetsPath}/../bundle.harmony.js`, { overwrite: true });
|
|
289
|
+
}
|
|
290
|
+
resolve();
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
reject(error);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return hermesResult;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Validate the intermediate directory before it is emptied and return the
|
|
301
|
+
* canonical path that was validated. The caller must use this returned path:
|
|
302
|
+
* validating one spelling and deleting through another re-opens symlink
|
|
303
|
+
* redirection.
|
|
304
|
+
*/
|
|
305
|
+
function assertSafeToEmpty(dir, cwd = process.cwd()) {
|
|
306
|
+
const requested = path_1.default.resolve(cwd, dir);
|
|
307
|
+
const lexicalCwd = path_1.default.resolve(cwd);
|
|
308
|
+
const lexicalHome = path_1.default.resolve(os_1.default.homedir());
|
|
309
|
+
const lexicalTmp = path_1.default.resolve(os_1.default.tmpdir());
|
|
310
|
+
const contains = (parent, child) => {
|
|
311
|
+
const relative = path_1.default.relative(parent, child);
|
|
312
|
+
return (relative === '' ||
|
|
313
|
+
(!relative.startsWith('..') && !path_1.default.isAbsolute(relative)));
|
|
314
|
+
};
|
|
315
|
+
const samePath = (left, right) => {
|
|
316
|
+
const normalize = (value) => {
|
|
317
|
+
const resolved = path_1.default.resolve(value);
|
|
318
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
319
|
+
};
|
|
320
|
+
return normalize(left) === normalize(right);
|
|
321
|
+
};
|
|
322
|
+
const reject = (target) => {
|
|
323
|
+
throw new Error((0, i18n_1.t)('unsafeIntermediateDir', { dir: target }));
|
|
324
|
+
};
|
|
325
|
+
const validate = (target, projectRoot, home, tmpRoot) => {
|
|
326
|
+
// Reject each protected directory and all of its ancestors.
|
|
327
|
+
for (const protectedPath of [
|
|
328
|
+
path_1.default.parse(target).root,
|
|
329
|
+
projectRoot,
|
|
330
|
+
home,
|
|
331
|
+
tmpRoot,
|
|
332
|
+
]) {
|
|
333
|
+
if (contains(target, protectedPath))
|
|
334
|
+
reject(target);
|
|
335
|
+
}
|
|
336
|
+
// Never allow build cleanup inside source-control metadata.
|
|
337
|
+
for (const metadata of ['.git', '.hg', '.svn']) {
|
|
338
|
+
if (contains(path_1.default.join(projectRoot, metadata), target))
|
|
339
|
+
reject(target);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
validate(requested, lexicalCwd, lexicalHome, lexicalTmp);
|
|
343
|
+
// A final-component symlink is the direct dangerous case: emptyDirSync would
|
|
344
|
+
// enumerate and remove entries from its target rather than removing the link.
|
|
345
|
+
if (fs.existsSync(requested) && fs.lstatSync(requested).isSymbolicLink()) {
|
|
346
|
+
reject(requested);
|
|
347
|
+
}
|
|
348
|
+
// emptyDirSync creates a missing directory too; create it first so it can be
|
|
349
|
+
// canonicalized and the exact validated path can be passed to deletion.
|
|
350
|
+
fs.ensureDirSync(requested);
|
|
351
|
+
const canonical = (value, allowWindowsLexicalFallback = false) => {
|
|
352
|
+
try {
|
|
353
|
+
return fs.realpathSync.native(value);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
// Windows 安全策略可能拒绝读取用户目录的最终句柄;这里的目录
|
|
357
|
+
// 已经通过词法边界校验,回退词法路径只用于比较受保护根目录,
|
|
358
|
+
// 不能替代目标目录本身的 canonicalize,避免重新打开 symlink 绕过。
|
|
359
|
+
if (process.platform === 'win32' && allowWindowsLexicalFallback) {
|
|
360
|
+
return path_1.default.resolve(value);
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const target = canonical(requested);
|
|
366
|
+
const canonicalCwd = canonical(lexicalCwd);
|
|
367
|
+
const canonicalHome = canonical(lexicalHome, true);
|
|
368
|
+
const canonicalTmp = canonical(lexicalTmp, true);
|
|
369
|
+
validate(target, canonicalCwd, canonicalHome, canonicalTmp);
|
|
370
|
+
// Reject a symlink in any component below a known safe base. System aliases
|
|
371
|
+
// such as macOS /var -> /private/var remain valid because the base itself is
|
|
372
|
+
// canonicalized before the relative suffix is appended.
|
|
373
|
+
for (const [lexicalBase, canonicalBase] of [
|
|
374
|
+
[lexicalCwd, canonicalCwd],
|
|
375
|
+
[lexicalHome, canonicalHome],
|
|
376
|
+
[lexicalTmp, canonicalTmp],
|
|
377
|
+
]) {
|
|
378
|
+
const relative = path_1.default.relative(lexicalBase, requested);
|
|
379
|
+
if (relative === '' ||
|
|
380
|
+
(!relative.startsWith('..') && !path_1.default.isAbsolute(relative))) {
|
|
381
|
+
if (!samePath(path_1.default.resolve(canonicalBase, relative), target)) {
|
|
382
|
+
reject(target);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return target;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Whether the app runs Hermes, i.e. whether the bundle must be compiled to
|
|
391
|
+
* bytecode. A JS bundle shipped to a Hermes app still runs, but starts slower
|
|
392
|
+
* and cannot use the Hermes base delta, so false negatives are what this
|
|
393
|
+
* guards against.
|
|
394
|
+
*
|
|
395
|
+
* Android: `hermesEnabled` in gradle.properties wins; then the legacy
|
|
396
|
+
* `project.ext.react.enableHermes` of build.gradle; with neither, the React
|
|
397
|
+
* Native gradle plugin (0.71+) defaults to Hermes and older versions to JSC.
|
|
398
|
+
* iOS: an installed `Pods/hermes-engine`; when `pod install` has not run on
|
|
399
|
+
* this machine (CI), Expo's `Podfile.properties.json` or the pod listed in
|
|
400
|
+
* `Podfile.lock` tell the same.
|
|
401
|
+
*/
|
|
402
|
+
async function detectHermesEnabled(platform, forceHermes, gradleConfig, projectRoot = process.cwd()) {
|
|
403
|
+
if (forceHermes) {
|
|
404
|
+
console.log((0, i18n_1.t)('forceHermes'));
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
if (platform === 'android') {
|
|
408
|
+
const gradleProperties = await readGradleProperties(path_1.default.join(projectRoot, 'android', 'gradle.properties'));
|
|
409
|
+
if (typeof gradleProperties.hermesEnabled === 'boolean') {
|
|
410
|
+
return gradleProperties.hermesEnabled;
|
|
411
|
+
}
|
|
412
|
+
if (typeof gradleConfig.enableHermes === 'boolean') {
|
|
413
|
+
return gradleConfig.enableHermes;
|
|
414
|
+
}
|
|
415
|
+
return reactNativeDefaultsToHermes(projectRoot);
|
|
416
|
+
}
|
|
417
|
+
if (platform === 'ios') {
|
|
418
|
+
return detectIosHermes(projectRoot);
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
/** parsed gradle.properties; a missing file is simply empty */
|
|
423
|
+
function readGradleProperties(file) {
|
|
424
|
+
return new Promise((resolve) => {
|
|
425
|
+
properties.parse(file, { path: true }, (error, props = {}) => {
|
|
426
|
+
if (error) {
|
|
427
|
+
if (error.code !== 'ENOENT') {
|
|
428
|
+
console.warn(`${file}: ${error.message ?? error}`);
|
|
429
|
+
}
|
|
430
|
+
resolve({});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
resolve(props);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** React Native's gradle plugin (0.71+) enables Hermes unless a property says otherwise */
|
|
438
|
+
function reactNativeDefaultsToHermes(projectRoot) {
|
|
439
|
+
const version = (0, dep_versions_1.getDepVersion)('react-native', projectRoot);
|
|
440
|
+
// major.minor by hand: React Native is 0.x, and the check must not depend
|
|
441
|
+
// on a semver library (tests replace compare-versions wholesale)
|
|
442
|
+
const match = version ? /^(\d+)\.(\d+)\./.exec(version) : null;
|
|
443
|
+
if (!match) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
const [major, minor] = [Number(match[1]), Number(match[2])];
|
|
447
|
+
return major > 0 || minor >= 71;
|
|
448
|
+
}
|
|
449
|
+
/** Remove Ruby comments without treating a # inside a quoted string as one. */
|
|
450
|
+
function stripRubyComments(source) {
|
|
451
|
+
return source
|
|
452
|
+
.split(/\r?\n/)
|
|
453
|
+
.map((line) => {
|
|
454
|
+
let quote;
|
|
455
|
+
let escaped = false;
|
|
456
|
+
for (let index = 0; index < line.length; index++) {
|
|
457
|
+
const char = line[index];
|
|
458
|
+
if (escaped) {
|
|
459
|
+
escaped = false;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (quote) {
|
|
463
|
+
if (char === '\\') {
|
|
464
|
+
escaped = true;
|
|
465
|
+
}
|
|
466
|
+
else if (char === quote) {
|
|
467
|
+
quote = undefined;
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (char === "'" || char === '"') {
|
|
472
|
+
quote = char;
|
|
473
|
+
}
|
|
474
|
+
else if (char === '#') {
|
|
475
|
+
return line.slice(0, index);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return line;
|
|
479
|
+
})
|
|
480
|
+
.join('\n');
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Read explicit iOS engine configuration. An explicit JSC/disabled-Hermes
|
|
484
|
+
* setting wins over installed pods because CocoaPods may still fetch the
|
|
485
|
+
* hermes-engine dependency for a JSC build.
|
|
486
|
+
*/
|
|
487
|
+
function configuredIosEngine(ios) {
|
|
488
|
+
let expoEngine;
|
|
489
|
+
try {
|
|
490
|
+
const value = JSON.parse(fs.readFileSync(path_1.default.join(ios, 'Podfile.properties.json'), 'utf8'))?.['expo.jsEngine'];
|
|
491
|
+
if (value === 'hermes' || value === 'jsc')
|
|
492
|
+
expoEngine = value;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// no Expo properties file
|
|
496
|
+
}
|
|
497
|
+
let podfile = '';
|
|
498
|
+
try {
|
|
499
|
+
podfile = fs.readFileSync(path_1.default.join(ios, 'Podfile'), 'utf8');
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// no Podfile
|
|
503
|
+
}
|
|
504
|
+
const activePodfile = stripRubyComments(podfile);
|
|
505
|
+
const envValue = (name) => new RegExp(`ENV\\s*\\[\\s*['"]${name}['"]\\s*\\]\\s*(?:\\|\\|=|=)\\s*['"]([^'"]+)['"]`, 'i')
|
|
506
|
+
.exec(activePodfile)?.[1]
|
|
507
|
+
?.toLowerCase();
|
|
508
|
+
const useHermes = envValue('USE_HERMES');
|
|
509
|
+
const useThirdPartyJsc = envValue('USE_THIRD_PARTY_JSC');
|
|
510
|
+
const hermesOption = /(?:^|[,(]\s*)(?::hermes_enabled\s*=>|hermes_enabled\s*:)\s*(true|false)\b/im
|
|
511
|
+
.exec(activePodfile)?.[1]
|
|
512
|
+
?.toLowerCase();
|
|
513
|
+
if (useThirdPartyJsc === '1' ||
|
|
514
|
+
useThirdPartyJsc === 'true' ||
|
|
515
|
+
useHermes === '0' ||
|
|
516
|
+
useHermes === 'false' ||
|
|
517
|
+
hermesOption === 'false' ||
|
|
518
|
+
expoEngine === 'jsc') {
|
|
519
|
+
return 'jsc';
|
|
520
|
+
}
|
|
521
|
+
if (useHermes === '1' ||
|
|
522
|
+
useHermes === 'true' ||
|
|
523
|
+
hermesOption === 'true' ||
|
|
524
|
+
expoEngine === 'hermes') {
|
|
525
|
+
return 'hermes';
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
/** Hermes on iOS: explicit configuration first, installed artifacts second. */
|
|
530
|
+
function detectIosHermes(projectRoot) {
|
|
531
|
+
const ios = path_1.default.join(projectRoot, 'ios');
|
|
532
|
+
const configured = configuredIosEngine(ios);
|
|
533
|
+
if (configured)
|
|
534
|
+
return configured === 'hermes';
|
|
535
|
+
if (fs.existsSync(path_1.default.join(ios, 'Pods', 'hermes-engine'))) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
// CI may not have run pod install, so use the lockfile only as weak evidence.
|
|
539
|
+
try {
|
|
540
|
+
const lock = fs.readFileSync(path_1.default.join(ios, 'Podfile.lock'), 'utf8');
|
|
541
|
+
if (/^\s*-\s+hermes-engine\b/m.test(lock))
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// no lockfile either
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
function getHermesOSBin() {
|
|
550
|
+
if (os_1.default.platform() === 'win32')
|
|
551
|
+
return 'win64-bin';
|
|
552
|
+
if (os_1.default.platform() === 'darwin')
|
|
553
|
+
return 'osx-bin';
|
|
554
|
+
if (os_1.default.platform() === 'linux')
|
|
555
|
+
return 'linux64-bin';
|
|
556
|
+
}
|
|
557
|
+
function getHermesExecutableName() {
|
|
558
|
+
return os_1.default.platform() === 'win32' ? 'hermesc.exe' : 'hermesc';
|
|
559
|
+
}
|
|
560
|
+
function dirnameOfPackage(packageJsonPath, projectRoot = process.cwd()) {
|
|
561
|
+
return path_1.default.dirname(require.resolve(packageJsonPath, {
|
|
562
|
+
paths: [projectRoot],
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
function assertSuccessfulSyncProcess(result, command) {
|
|
566
|
+
if (result.error) {
|
|
567
|
+
throw result.error;
|
|
568
|
+
}
|
|
569
|
+
if (result.status !== 0) {
|
|
570
|
+
throw new Error(`${command} failed with ${result.status === null
|
|
571
|
+
? `signal ${result.signal}`
|
|
572
|
+
: `exit code ${result.status}`}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function normalizeString(value) {
|
|
576
|
+
const normalized = value?.trim();
|
|
577
|
+
return normalized ? normalized : undefined;
|
|
578
|
+
}
|
|
579
|
+
function resolveHermesCommand(projectRoot = process.cwd()) {
|
|
580
|
+
const osBin = getHermesOSBin();
|
|
581
|
+
if (!osBin) {
|
|
582
|
+
throw new Error((0, i18n_1.t)('unsupportedPlatformForHermes', { platform: os_1.default.platform() }));
|
|
583
|
+
}
|
|
584
|
+
const executableName = getHermesExecutableName();
|
|
585
|
+
const candidates = [];
|
|
586
|
+
try {
|
|
587
|
+
const rnDir = dirnameOfPackage('react-native/package.json', projectRoot);
|
|
588
|
+
candidates.push(path_1.default.join(rnDir, 'sdks', 'hermesc', osBin, executableName));
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// react-native is required for normal RN projects; keep looking so the
|
|
592
|
+
// final error can include all candidates we were able to infer.
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const hermesCompilerDir = dirnameOfPackage('hermes-compiler/package.json', projectRoot);
|
|
596
|
+
candidates.push(path_1.default.join(hermesCompilerDir, 'hermesc', osBin, executableName));
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// RN 0.85+ uses hermes-compiler, older projects may still use other paths.
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const hermesEngineDir = dirnameOfPackage('hermes-engine/package.json', projectRoot);
|
|
603
|
+
candidates.push(path_1.default.join(hermesEngineDir, osBin, executableName), path_1.default.join(hermesEngineDir, 'hermesc', osBin, executableName));
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// RN 0.70-era projects commonly used hermes-engine; optional for newer RN.
|
|
607
|
+
}
|
|
608
|
+
const hermesCommand = candidates.find((candidate) => fs.existsSync(candidate));
|
|
609
|
+
if (hermesCommand) {
|
|
610
|
+
return hermesCommand;
|
|
611
|
+
}
|
|
612
|
+
throw new Error(`Cannot find hermesc. Tried:\n${candidates.map((candidate) => `- ${candidate}`).join('\n')}`);
|
|
613
|
+
}
|
|
614
|
+
async function checkGradleConfig(projectRoot) {
|
|
615
|
+
// undefined when build.gradle does not mention Hermes at all (React Native
|
|
616
|
+
// 0.71+ moved the switch to gradle.properties; see detectHermesEnabled)
|
|
617
|
+
let enableHermes;
|
|
618
|
+
let crunchPngs;
|
|
619
|
+
try {
|
|
620
|
+
const gradleConfig = await g2js.parseFile(path_1.default.join(projectRoot, 'android', 'app', 'build.gradle'));
|
|
621
|
+
crunchPngs = gradleConfig.android.buildTypes.release.crunchPngs;
|
|
622
|
+
const projectConfig = gradleConfig['project.ext.react'];
|
|
623
|
+
if (projectConfig) {
|
|
624
|
+
for (const packagerConfig of projectConfig) {
|
|
625
|
+
if (packagerConfig.includes('enableHermes')) {
|
|
626
|
+
enableHermes = packagerConfig.includes('true');
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// ignore parsing failures
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
enableHermes,
|
|
637
|
+
crunchPngs,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* hermesc argument policy. `-output-source-map` is always passed: hermesc then
|
|
642
|
+
* strips the debug info section from the bytecode (like React Native's own
|
|
643
|
+
* release builds, 15-40% smaller bytecode and smaller hot-update patches) and
|
|
644
|
+
* writes its own `<bundle>.map` next to the output. When the user asked for a
|
|
645
|
+
* sourcemap that file is composed with the packager map as before; otherwise it
|
|
646
|
+
* simply stays in the intermediate directory (never packed into the ppk) so
|
|
647
|
+
* `address at` stack frames can still be symbolicated later.
|
|
648
|
+
*/
|
|
649
|
+
/**
|
|
650
|
+
* hermesc echoes every diagnostic as three lines (header, the offending source
|
|
651
|
+
* line, a caret line). Minified bundles put a whole module on one line, so a
|
|
652
|
+
* single warning can be 100k+ characters and blindly keeping the tail of stderr
|
|
653
|
+
* buries the actual error under warning noise. Keep the diagnostic headers,
|
|
654
|
+
* prefer errors over warnings, and cap the length of whatever survives.
|
|
655
|
+
*/
|
|
656
|
+
function summarizeHermescStderr(stderr) {
|
|
657
|
+
const raw = String(stderr ?? '').split(/\r?\n/);
|
|
658
|
+
const kept = [];
|
|
659
|
+
for (let i = 0; i < raw.length; i++) {
|
|
660
|
+
const line = raw[i].trim();
|
|
661
|
+
if (!line)
|
|
662
|
+
continue;
|
|
663
|
+
if (/^\^~*$/.test(line))
|
|
664
|
+
continue; // caret line
|
|
665
|
+
// the line before a caret is the echoed source, never the message
|
|
666
|
+
const next = raw[i + 1]?.trim();
|
|
667
|
+
if (next && /^\^~*$/.test(next))
|
|
668
|
+
continue;
|
|
669
|
+
kept.push(line.length > 300 ? `${line.slice(0, 300)}…` : line);
|
|
670
|
+
}
|
|
671
|
+
if (kept.length === 0)
|
|
672
|
+
return '';
|
|
673
|
+
const isError = (line) => /(?::\s*|^)(?:fatal error|error)\b/i.test(line) ||
|
|
674
|
+
/^(?:Assertion|Stack dump|PLEASE submit a bug report)/i.test(line);
|
|
675
|
+
const errors = kept.filter(isError);
|
|
676
|
+
if (errors.length > 0)
|
|
677
|
+
return errors.slice(-3).join(' ');
|
|
678
|
+
const notWarnings = kept.filter((line) => !/:\s*(?:warning|note):/i.test(line));
|
|
679
|
+
return (notWarnings.length > 0 ? notWarnings : kept).slice(-3).join(' ');
|
|
680
|
+
}
|
|
681
|
+
/** hermesc arguments writing `bundlePath` from `input` (in place by default) */
|
|
682
|
+
function buildHermescArgs(bundlePath, input = bundlePath) {
|
|
683
|
+
return [
|
|
684
|
+
'-emit-binary',
|
|
685
|
+
'-out',
|
|
686
|
+
bundlePath,
|
|
687
|
+
input,
|
|
688
|
+
'-O',
|
|
689
|
+
'-output-source-map',
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
/** captured stderr is bounded to this much head and this much tail */
|
|
693
|
+
const STDERR_KEEP_BYTES = 512 * 1024;
|
|
694
|
+
/** spawn without blocking the event loop, so two hermesc runs can overlap */
|
|
695
|
+
function runProcess(command, args, captureStderr) {
|
|
696
|
+
return new Promise((resolve) => {
|
|
697
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
698
|
+
stdio: ['ignore', 'ignore', captureStderr ? 'pipe' : 'ignore'],
|
|
699
|
+
});
|
|
700
|
+
// hermesc echoes whole (minified) source lines per diagnostic: keep the
|
|
701
|
+
// head and the tail, never an unbounded transcript
|
|
702
|
+
const head = [];
|
|
703
|
+
const tail = [];
|
|
704
|
+
let headBytes = 0;
|
|
705
|
+
let tailBytes = 0;
|
|
706
|
+
let dropped = false;
|
|
707
|
+
child.stderr?.on('data', (chunk) => {
|
|
708
|
+
if (headBytes < STDERR_KEEP_BYTES) {
|
|
709
|
+
head.push(chunk);
|
|
710
|
+
headBytes += chunk.length;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
tail.push(chunk);
|
|
714
|
+
tailBytes += chunk.length;
|
|
715
|
+
while (tail.length > 1 && tailBytes > STDERR_KEEP_BYTES) {
|
|
716
|
+
tailBytes -= tail.shift().length;
|
|
717
|
+
dropped = true;
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
let settled = false;
|
|
721
|
+
const finish = (outcome) => {
|
|
722
|
+
if (settled)
|
|
723
|
+
return;
|
|
724
|
+
settled = true;
|
|
725
|
+
resolve(outcome);
|
|
726
|
+
};
|
|
727
|
+
child.once('error', (error) => finish({ status: null, signal: null, stderr: '', error }));
|
|
728
|
+
child.once('close', (status, signal) => finish({
|
|
729
|
+
status,
|
|
730
|
+
signal,
|
|
731
|
+
stderr: Buffer.concat(head).toString() +
|
|
732
|
+
(dropped ? '\n[... output truncated ...]\n' : '') +
|
|
733
|
+
Buffer.concat(tail).toString(),
|
|
734
|
+
}));
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
/** how long the compile phase waits for a base that is still being fetched */
|
|
738
|
+
const HERMES_BASE_WAIT_MS = 60_000;
|
|
739
|
+
/**
|
|
740
|
+
* The selection runs while Metro bundles; when Metro is done first, wait a
|
|
741
|
+
* bounded time for it — a stalled download must never hold up the compile.
|
|
742
|
+
*/
|
|
743
|
+
async function awaitPendingBase(pending) {
|
|
744
|
+
const waitMs = Number(process.env.PAKTA_HERMES_BASE_WAIT_MS) || HERMES_BASE_WAIT_MS;
|
|
745
|
+
let timer;
|
|
746
|
+
const gaveUp = new Promise((resolve) => {
|
|
747
|
+
timer = setTimeout(() => resolve({
|
|
748
|
+
base: null,
|
|
749
|
+
bytecodeVersion: null,
|
|
750
|
+
logs: [
|
|
751
|
+
(0, i18n_1.t)('hermesBaseNone', {
|
|
752
|
+
reason: `base not ready after ${Math.round(waitMs / 1000)}s`,
|
|
753
|
+
}),
|
|
754
|
+
],
|
|
755
|
+
}), waitMs);
|
|
756
|
+
});
|
|
757
|
+
try {
|
|
758
|
+
return await Promise.race([pending, gaveUp]);
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
clearTimeout(timer);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/** plain compile of `input` into `out`; throws when hermesc fails */
|
|
765
|
+
async function compilePlain(hermesCommand, out, input = out) {
|
|
766
|
+
const args = buildHermescArgs(out, input);
|
|
767
|
+
console.log((0, i18n_1.t)('runningHermesc', { command: hermesCommand, args: args.join(' ') }));
|
|
768
|
+
assertSuccessfulSyncProcess(await runProcess(hermesCommand, args, false), hermesCommand);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Merge the packager map with the hermes map into `sourcemapOutput` (which is
|
|
772
|
+
* normally the hermes map's own path). Resolves false when React Native's
|
|
773
|
+
* composer cannot be found; throws when it fails.
|
|
774
|
+
*/
|
|
775
|
+
async function composeSourceMaps(packagerMap, hermesMap, sourcemapOutput) {
|
|
776
|
+
let composerPath;
|
|
777
|
+
try {
|
|
778
|
+
// resolve through the project so hoisted node_modules (monorepos) work.
|
|
779
|
+
// Extensionless first: RN >= 0.87 maps "./scripts/*" to "./scripts/*.js"
|
|
780
|
+
// in its exports, so the ".js" specifier resolves to ".js.js" and
|
|
781
|
+
// fails; CJS resolution adds the extension itself on older RN.
|
|
782
|
+
composerPath = require.resolve('react-native/scripts/compose-source-maps', {
|
|
783
|
+
paths: [process.cwd()],
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
try {
|
|
788
|
+
composerPath = require.resolve('react-native/scripts/compose-source-maps.js', { paths: [process.cwd()] });
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
console.warn((0, i18n_1.t)('composeSourceMapsNotFound'));
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
console.log((0, i18n_1.t)('composingSourceMap'));
|
|
796
|
+
assertSuccessfulSyncProcess(await runProcess((0, runtime_1.getJavaScriptRuntime)(), [composerPath, packagerMap, hermesMap, '-o', sourcemapOutput], false), composerPath);
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
async function compileHermesByteCode({ bundleName, outputFolder, sourcemapOutput, shouldCleanSourcemap, baseRequest, pendingBase, hermesCommand, }) {
|
|
800
|
+
console.log((0, i18n_1.t)('hermesEnabledCompiling'));
|
|
801
|
+
const bundlePath = path_1.default.join(outputFolder, bundleName);
|
|
802
|
+
const hermesMap = `${bundlePath}.map`;
|
|
803
|
+
const packagerMap = path_1.default.join(outputFolder, `${bundleName}.txt.map`);
|
|
804
|
+
if (sourcemapOutput) {
|
|
805
|
+
// hermesc is about to write its own map at `<bundle>.map`, which is where
|
|
806
|
+
// the packager map normally sits: move it aside (copy only when elsewhere)
|
|
807
|
+
if (path_1.default.resolve(sourcemapOutput) === path_1.default.resolve(hermesMap)) {
|
|
808
|
+
fs.renameSync(sourcemapOutput, packagerMap);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
fs.copyFileSync(sourcemapOutput, packagerMap);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
console.log((0, i18n_1.t)('hermesSourcemapKept', { file: hermesMap }));
|
|
816
|
+
}
|
|
817
|
+
// the selection already resolved hermesc while Metro ran; only look it up
|
|
818
|
+
// again when there was no selection (or it could not find the binary)
|
|
819
|
+
let selection = pendingBase ? await awaitPendingBase(pendingBase) : null;
|
|
820
|
+
const command = hermesCommand ?? selection?.hermesCommand ?? resolveHermesCommand();
|
|
821
|
+
if (!selection || selection.commandUnavailable) {
|
|
822
|
+
selection = await selectHermesBase(command, baseRequest);
|
|
823
|
+
}
|
|
824
|
+
for (const line of selection.logs)
|
|
825
|
+
console.log(line);
|
|
826
|
+
const result = {
|
|
827
|
+
bytecodeVersion: selection.bytecodeVersion,
|
|
828
|
+
base: null,
|
|
829
|
+
outcome: 'none',
|
|
830
|
+
};
|
|
831
|
+
const base = selection.base;
|
|
832
|
+
// the packager + hermes maps merged into sourcemapOutput
|
|
833
|
+
let composed = false;
|
|
834
|
+
if (!base) {
|
|
835
|
+
await compilePlain(command, bundlePath);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
// The JS bundle moves into a work dir and both hermesc runs read it there
|
|
839
|
+
// (no copies); the base compile writes the real output, and with
|
|
840
|
+
// verification on the plain compile the check needs runs concurrently
|
|
841
|
+
// into `plain/` — it simply becomes the result when the base is rejected,
|
|
842
|
+
// so a rejected base never costs a third compile.
|
|
843
|
+
const workDir = path_1.default.join(outputFolder, '.hermes-base');
|
|
844
|
+
const jsSource = path_1.default.join(workDir, bundleName);
|
|
845
|
+
const plainPath = path_1.default.join(workDir, 'plain', bundleName);
|
|
846
|
+
const wantPlain = Boolean(baseRequest?.verify);
|
|
847
|
+
fs.ensureDirSync(path_1.default.dirname(plainPath));
|
|
848
|
+
fs.renameSync(bundlePath, jsSource);
|
|
849
|
+
const args = (0, hermes_base_1.hermescArgsWithBase)(buildHermescArgs(bundlePath, jsSource), base.path);
|
|
850
|
+
console.log((0, i18n_1.t)('runningHermesc', { command, args: args.join(' ') }));
|
|
851
|
+
const [attempt, plain] = await Promise.all([
|
|
852
|
+
// -w: warnings only bloat the captured stderr, errors still print
|
|
853
|
+
runProcess(command, [...args, '-w'], true),
|
|
854
|
+
wantPlain
|
|
855
|
+
? runProcess(command, buildHermescArgs(plainPath, jsSource), false)
|
|
856
|
+
: Promise.resolve(null),
|
|
857
|
+
]);
|
|
858
|
+
let usedBase = attempt.status === 0 && !attempt.error;
|
|
859
|
+
if (!usedBase) {
|
|
860
|
+
const fullStderr = attempt.error
|
|
861
|
+
? String(attempt.error.message ?? attempt.error)
|
|
862
|
+
: attempt.stderr;
|
|
863
|
+
const reason = summarizeHermescStderr(fullStderr);
|
|
864
|
+
console.warn((0, i18n_1.t)('hermesBaseCompileFailed', {
|
|
865
|
+
reason: reason || `exit ${attempt.status}`,
|
|
866
|
+
}));
|
|
867
|
+
result.outcomeDetail = `base compile failed: ${reason || `exit ${attempt.status}`}`;
|
|
868
|
+
if (fullStderr.trim()) {
|
|
869
|
+
// the summary drops noise; keep everything for bug reports — next to
|
|
870
|
+
// the intermediate dir, never inside it (its content is packed)
|
|
871
|
+
const logPath = hermesBaseErrorLogPath(outputFolder);
|
|
872
|
+
try {
|
|
873
|
+
fs.writeFileSync(logPath, fullStderr);
|
|
874
|
+
console.warn((0, i18n_1.t)('hermesBaseCompileFailedLog', { file: logPath }));
|
|
875
|
+
}
|
|
876
|
+
catch { }
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const plainOk = plain !== null && plain.status === 0 && !plain.error;
|
|
880
|
+
// compose the base output's map while the disassembly compare runs; it is
|
|
881
|
+
// redone from the plain map in the rare case the base is rejected
|
|
882
|
+
const speculativeCompose = usedBase && sourcemapOutput
|
|
883
|
+
? composeSourceMaps(packagerMap, hermesMap, sourcemapOutput)
|
|
884
|
+
: null;
|
|
885
|
+
if (usedBase && wantPlain) {
|
|
886
|
+
if (!plainOk) {
|
|
887
|
+
// The check could not run. That is not evidence against the base,
|
|
888
|
+
// but verification was asked for and did not happen, so the base is
|
|
889
|
+
// dropped exactly like a dump that could not be read: never ship
|
|
890
|
+
// unverified when asked to verify. The plain compile is simply
|
|
891
|
+
// repeated below (compilePlain), which surfaces a real compiler
|
|
892
|
+
// failure as the build error it is.
|
|
893
|
+
const reason = (plain?.error && String(plain.error.message ?? plain.error)) ||
|
|
894
|
+
`exit ${plain?.status}`;
|
|
895
|
+
console.warn((0, i18n_1.t)('hermesBasePlainCompileFailed', { reason }));
|
|
896
|
+
result.outcome = 'dump-failed';
|
|
897
|
+
result.outcomeDetail = `plain compile failed: ${reason}`;
|
|
898
|
+
usedBase = false;
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
const dumpTo = hermesBaseDumpPaths(outputFolder);
|
|
902
|
+
let outcome;
|
|
903
|
+
try {
|
|
904
|
+
outcome = await (0, hermes_base_1.compareHermesBytecode)(command, bundlePath, plainPath, {
|
|
905
|
+
dumpTo,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
outcome = {
|
|
910
|
+
status: 'dump-failed',
|
|
911
|
+
detail: String(error?.message ?? error),
|
|
912
|
+
functions: 0,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
result.verified = outcome.status === 'equivalent';
|
|
916
|
+
if (outcome.status !== 'equivalent') {
|
|
917
|
+
result.outcome =
|
|
918
|
+
outcome.status === 'different' ? 'rejected' : 'dump-failed';
|
|
919
|
+
result.outcomeDetail = outcome.detail;
|
|
920
|
+
}
|
|
921
|
+
if (outcome.status === 'equivalent') {
|
|
922
|
+
console.log((0, i18n_1.t)('hermesBaseVerified', { functions: outcome.functions }));
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// a dump that could not be read is not evidence either way; the
|
|
926
|
+
// base is still dropped (never ship unverified when asked to
|
|
927
|
+
// verify), but the log says which case this is
|
|
928
|
+
console.warn((0, i18n_1.t)(outcome.status === 'different'
|
|
929
|
+
? 'hermesBaseVerifyFailed'
|
|
930
|
+
: 'hermesBaseVerifyDumpFailed', { detail: outcome.detail ?? '' }));
|
|
931
|
+
if (dumpTo) {
|
|
932
|
+
console.warn((0, i18n_1.t)('hermesBaseVerifyDumpsWritten', {
|
|
933
|
+
withBase: dumpTo.withBase,
|
|
934
|
+
plain: dumpTo.plain,
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
usedBase = false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (speculativeCompose) {
|
|
942
|
+
const done = await speculativeCompose.catch((error) => {
|
|
943
|
+
if (usedBase)
|
|
944
|
+
throw error;
|
|
945
|
+
return false;
|
|
946
|
+
});
|
|
947
|
+
composed = usedBase && done;
|
|
948
|
+
}
|
|
949
|
+
if (!usedBase) {
|
|
950
|
+
if (plainOk) {
|
|
951
|
+
fs.moveSync(plainPath, bundlePath, { overwrite: true });
|
|
952
|
+
if (fs.existsSync(`${plainPath}.map`)) {
|
|
953
|
+
fs.moveSync(`${plainPath}.map`, hermesMap, { overwrite: true });
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
await compilePlain(command, bundlePath, jsSource);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
fs.removeSync(workDir);
|
|
961
|
+
result.base = usedBase ? base : null;
|
|
962
|
+
if (usedBase)
|
|
963
|
+
result.outcome = 'used';
|
|
964
|
+
}
|
|
965
|
+
if (sourcemapOutput && !composed) {
|
|
966
|
+
await composeSourceMaps(packagerMap, hermesMap, sourcemapOutput);
|
|
967
|
+
}
|
|
968
|
+
if (shouldCleanSourcemap) {
|
|
969
|
+
fs.removeSync(packagerMap);
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
/** where a failed base attempt's full hermesc output goes (never packed) */
|
|
974
|
+
function hermesBaseErrorLogPath(outputFolder) {
|
|
975
|
+
return path_1.default.join(path_1.default.dirname(path_1.default.resolve(outputFolder)), 'hermes-base-error.log');
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* With PAKTA_HERMES_BASE_DEBUG set, both disassemblies of the equivalence
|
|
979
|
+
* check are kept next to the intermediate directory (never packed) so a
|
|
980
|
+
* rejected base can be reported with the exact difference.
|
|
981
|
+
*/
|
|
982
|
+
function hermesBaseDumpPaths(outputFolder, env = process.env) {
|
|
983
|
+
const flag = env.PAKTA_HERMES_BASE_DEBUG?.trim().toLowerCase();
|
|
984
|
+
if (!flag || flag === '0' || flag === 'false')
|
|
985
|
+
return undefined;
|
|
986
|
+
const dir = path_1.default.dirname(path_1.default.resolve(outputFolder));
|
|
987
|
+
return {
|
|
988
|
+
withBase: path_1.default.join(dir, 'hermes-base-dump-base.txt'),
|
|
989
|
+
plain: path_1.default.join(dir, 'hermes-base-dump-plain.txt'),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Begin resolving the base right away (before Metro); the compile phase awaits
|
|
994
|
+
* the result. Never rejects: any failure is reported there instead.
|
|
995
|
+
*/
|
|
996
|
+
function startHermesBaseSelection(request) {
|
|
997
|
+
return (async () => {
|
|
998
|
+
let hermesCommand;
|
|
999
|
+
try {
|
|
1000
|
+
hermesCommand = resolveHermesCommand();
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
return {
|
|
1004
|
+
base: null,
|
|
1005
|
+
bytecodeVersion: null,
|
|
1006
|
+
logs: [],
|
|
1007
|
+
commandUnavailable: true,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
return await selectHermesBase(hermesCommand, request);
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
return {
|
|
1015
|
+
base: null,
|
|
1016
|
+
bytecodeVersion: null,
|
|
1017
|
+
logs: [
|
|
1018
|
+
(0, i18n_1.t)('hermesBaseNone', { reason: error?.message ?? String(error) }),
|
|
1019
|
+
],
|
|
1020
|
+
hermesCommand,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
})();
|
|
1024
|
+
}
|
|
1025
|
+
async function selectHermesBase(hermesCommand, request) {
|
|
1026
|
+
const logs = [];
|
|
1027
|
+
const none = (bytecodeVersion = null) => ({
|
|
1028
|
+
base: null,
|
|
1029
|
+
bytecodeVersion,
|
|
1030
|
+
logs,
|
|
1031
|
+
hermesCommand,
|
|
1032
|
+
});
|
|
1033
|
+
if (!request || request.option === 'none') {
|
|
1034
|
+
if (request) {
|
|
1035
|
+
logs.push((0, i18n_1.t)('hermesBaseNone', { reason: 'disabled by option' }));
|
|
1036
|
+
}
|
|
1037
|
+
return none();
|
|
1038
|
+
}
|
|
1039
|
+
const gate = (0, hermes_base_1.classifyHermesCommand)(hermesCommand);
|
|
1040
|
+
if (!gate.allowed) {
|
|
1041
|
+
logs.push((0, i18n_1.t)('hermesBaseNone', { reason: gate.reason ?? 'hermesc not eligible' }));
|
|
1042
|
+
return none();
|
|
1043
|
+
}
|
|
1044
|
+
const bytecodeVersion = (0, hermes_base_1.probeHbcVersion)(hermesCommand);
|
|
1045
|
+
if (bytecodeVersion === null) {
|
|
1046
|
+
logs.push((0, i18n_1.t)('hermesBaseNone', { reason: 'could not probe HBC version' }));
|
|
1047
|
+
return none();
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const base = await (0, hermes_base_1.resolveHermesBase)({
|
|
1051
|
+
option: request.option,
|
|
1052
|
+
hermesCommand,
|
|
1053
|
+
bytecodeVersion,
|
|
1054
|
+
appId: request.appId,
|
|
1055
|
+
cacheMaxMb: request.cacheMaxMb,
|
|
1056
|
+
fetchBase: api_1.getHermesBase,
|
|
1057
|
+
log: (message) => logs.push(message),
|
|
1058
|
+
});
|
|
1059
|
+
return { base, bytecodeVersion, logs, hermesCommand };
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
logs.push((0, i18n_1.t)('hermesBaseNone', { reason: error?.message ?? String(error) }));
|
|
1063
|
+
return none(bytecodeVersion);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function copyDebugidForSentry(bundleName, outputFolder, sourcemapOutput) {
|
|
1067
|
+
if (sourcemapOutput) {
|
|
1068
|
+
let copyDebugidPath;
|
|
1069
|
+
try {
|
|
1070
|
+
copyDebugidPath = require.resolve('@sentry/react-native/scripts/copy-debugid.js', {
|
|
1071
|
+
paths: [process.cwd()],
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
catch {
|
|
1075
|
+
console.error((0, i18n_1.t)('sentryReactNativeNotFound'));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (!fs.existsSync(copyDebugidPath)) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
console.log((0, i18n_1.t)('copyingDebugId'));
|
|
1082
|
+
assertSuccessfulSyncProcess((0, runtime_1.spawnJavaScriptSync)([
|
|
1083
|
+
copyDebugidPath,
|
|
1084
|
+
path_1.default.join(outputFolder, `${bundleName}.txt.map`),
|
|
1085
|
+
path_1.default.join(outputFolder, `${bundleName}.map`),
|
|
1086
|
+
], {
|
|
1087
|
+
stdio: 'ignore',
|
|
1088
|
+
}), copyDebugidPath);
|
|
1089
|
+
}
|
|
1090
|
+
fs.removeSync(path_1.default.join(outputFolder, `${bundleName}.txt.map`));
|
|
1091
|
+
}
|
|
1092
|
+
async function prepareSentryUploadArtifacts(bundleName, outputFolder, platform) {
|
|
1093
|
+
const bundlePath = path_1.default.join(outputFolder, bundleName);
|
|
1094
|
+
const sourcemapPath = path_1.default.join(outputFolder, `${bundleName}.map`);
|
|
1095
|
+
if (platform !== 'android' || bundleName === ANDROID_SENTRY_BUNDLE_NAME) {
|
|
1096
|
+
return {
|
|
1097
|
+
bundlePath,
|
|
1098
|
+
sourcemapPath,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const androidBundlePath = path_1.default.join(outputFolder, ANDROID_SENTRY_BUNDLE_NAME);
|
|
1102
|
+
const androidSourcemapPath = path_1.default.join(outputFolder, `${ANDROID_SENTRY_BUNDLE_NAME}.map`);
|
|
1103
|
+
await fs.promises.copyFile(bundlePath, androidBundlePath);
|
|
1104
|
+
const sourcemap = JSON.parse(await fs.promises.readFile(sourcemapPath, 'utf8'));
|
|
1105
|
+
sourcemap.file = ANDROID_SENTRY_BUNDLE_NAME;
|
|
1106
|
+
await fs.promises.writeFile(androidSourcemapPath, JSON.stringify(sourcemap));
|
|
1107
|
+
return {
|
|
1108
|
+
bundlePath: androidBundlePath,
|
|
1109
|
+
sourcemapPath: androidSourcemapPath,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
async function readSourcemapDebugId(sourcemapPath) {
|
|
1113
|
+
try {
|
|
1114
|
+
const sourcemap = JSON.parse(await fs.promises.readFile(sourcemapPath, 'utf8'));
|
|
1115
|
+
const debugId = sourcemap.debugId ?? sourcemap.debug_id;
|
|
1116
|
+
return typeof debugId === 'string' ? normalizeString(debugId) : undefined;
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function resolveSentryReleaseFromValues(releaseValue, distValue) {
|
|
1123
|
+
const release = normalizeString(releaseValue);
|
|
1124
|
+
if (!release) {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
release,
|
|
1129
|
+
dist: normalizeString(distValue),
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
async function resolveSentryUploadMode(sourcemapPath, options = {}) {
|
|
1133
|
+
const optionRelease = resolveSentryReleaseFromValues(options.sentryRelease, options.sentryDist);
|
|
1134
|
+
if (optionRelease) {
|
|
1135
|
+
return {
|
|
1136
|
+
type: 'release',
|
|
1137
|
+
...optionRelease,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const debugId = await readSourcemapDebugId(sourcemapPath);
|
|
1141
|
+
if (debugId) {
|
|
1142
|
+
return {
|
|
1143
|
+
type: 'debug-id',
|
|
1144
|
+
debugId,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const environmentRelease = resolveSentryReleaseFromValues(process.env.SENTRY_RELEASE, process.env.SENTRY_DIST);
|
|
1148
|
+
if (environmentRelease) {
|
|
1149
|
+
return {
|
|
1150
|
+
type: 'release',
|
|
1151
|
+
...environmentRelease,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
throw new Error('[pakta/sentry] Generated source map does not contain a Debug ID. ' +
|
|
1155
|
+
'Add @sentry/react-native/metro to metro.config.js so the OTA bundle can be matched by Debug ID, ' +
|
|
1156
|
+
'or set --sentry-release/--sentry-dist (or SENTRY_RELEASE/SENTRY_DIST) for legacy release matching.');
|
|
1157
|
+
}
|
|
1158
|
+
function buildSentrySourcemapsUploadArgs({ sentryCliPath, bundlePath, sourcemapPath, release, dist, stripPrefix = process.cwd(), debugIdReference = false, useStandaloneSourcemapsCommand = true, }) {
|
|
1159
|
+
const uploadArgs = ['--strip-prefix', stripPrefix, bundlePath, sourcemapPath];
|
|
1160
|
+
if (debugIdReference) {
|
|
1161
|
+
if (!useStandaloneSourcemapsCommand) {
|
|
1162
|
+
throw new Error('[pakta/sentry] Debug ID upload requires sentry-cli sourcemaps upload.');
|
|
1163
|
+
}
|
|
1164
|
+
return [
|
|
1165
|
+
sentryCliPath,
|
|
1166
|
+
'sourcemaps',
|
|
1167
|
+
'upload',
|
|
1168
|
+
'--debug-id-reference',
|
|
1169
|
+
...uploadArgs,
|
|
1170
|
+
];
|
|
1171
|
+
}
|
|
1172
|
+
if (!release) {
|
|
1173
|
+
throw new Error('[pakta/sentry] Legacy Sentry sourcemap upload requires a release.');
|
|
1174
|
+
}
|
|
1175
|
+
if (!useStandaloneSourcemapsCommand) {
|
|
1176
|
+
return [
|
|
1177
|
+
sentryCliPath,
|
|
1178
|
+
'releases',
|
|
1179
|
+
'files',
|
|
1180
|
+
release,
|
|
1181
|
+
'upload-sourcemaps',
|
|
1182
|
+
...(dist ? ['--dist', dist] : []),
|
|
1183
|
+
...uploadArgs,
|
|
1184
|
+
];
|
|
1185
|
+
}
|
|
1186
|
+
return [
|
|
1187
|
+
sentryCliPath,
|
|
1188
|
+
'sourcemaps',
|
|
1189
|
+
'upload',
|
|
1190
|
+
'--release',
|
|
1191
|
+
release,
|
|
1192
|
+
...(dist ? ['--dist', dist] : []),
|
|
1193
|
+
...uploadArgs,
|
|
1194
|
+
];
|
|
1195
|
+
}
|
|
1196
|
+
function supportsStandaloneSentrySourcemapsUpload(sentryCliPath) {
|
|
1197
|
+
const result = (0, runtime_1.spawnJavaScriptSync)([sentryCliPath, 'sourcemaps', 'upload', '--help'], {
|
|
1198
|
+
stdio: 'ignore',
|
|
1199
|
+
});
|
|
1200
|
+
return !result.error && result.status === 0;
|
|
1201
|
+
}
|
|
1202
|
+
function supportsSentryDebugIdReference(sentryCliPath) {
|
|
1203
|
+
const result = (0, runtime_1.spawnJavaScriptSync)([sentryCliPath, 'sourcemaps', 'upload', '--help'], {
|
|
1204
|
+
encoding: 'utf8',
|
|
1205
|
+
});
|
|
1206
|
+
return (!result.error &&
|
|
1207
|
+
result.status === 0 &&
|
|
1208
|
+
typeof result.stdout === 'string' &&
|
|
1209
|
+
result.stdout.includes('--debug-id-reference'));
|
|
1210
|
+
}
|
|
1211
|
+
function runSentryCli(args) {
|
|
1212
|
+
return (0, runtime_1.spawnJavaScriptSync)(args, {
|
|
1213
|
+
stdio: 'inherit',
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function uploadSourcemapsWithRelease({ sentryCliPath, bundlePath, sourcemapPath, release, dist, useStandaloneSourcemapsCommand, }) {
|
|
1217
|
+
assertSuccessfulSyncProcess(runSentryCli([sentryCliPath, 'releases', 'set-commits', release, '--auto']), sentryCliPath);
|
|
1218
|
+
console.log((0, i18n_1.t)('sentryReleaseCreated', { version: release }));
|
|
1219
|
+
console.log((0, i18n_1.t)('uploadingSourcemap'));
|
|
1220
|
+
assertSuccessfulSyncProcess(runSentryCli(buildSentrySourcemapsUploadArgs({
|
|
1221
|
+
sentryCliPath,
|
|
1222
|
+
bundlePath,
|
|
1223
|
+
sourcemapPath,
|
|
1224
|
+
release,
|
|
1225
|
+
dist,
|
|
1226
|
+
useStandaloneSourcemapsCommand,
|
|
1227
|
+
})), sentryCliPath);
|
|
1228
|
+
}
|
|
1229
|
+
async function uploadSourcemapForSentry(bundleName, outputFolder, sourcemapOutput, platform = '', sentryOptions = {}) {
|
|
1230
|
+
if (!sourcemapOutput) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
let sentryCliPath;
|
|
1234
|
+
try {
|
|
1235
|
+
sentryCliPath = require.resolve('@sentry/cli/bin/sentry-cli', {
|
|
1236
|
+
paths: [process.cwd()],
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
console.error((0, i18n_1.t)('sentryCliNotFound'));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (!fs.existsSync(sentryCliPath)) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const { bundlePath, sourcemapPath } = await prepareSentryUploadArtifacts(bundleName, outputFolder, platform);
|
|
1247
|
+
const uploadMode = await resolveSentryUploadMode(sourcemapPath, sentryOptions);
|
|
1248
|
+
const useStandaloneSourcemapsCommand = supportsStandaloneSentrySourcemapsUpload(sentryCliPath);
|
|
1249
|
+
if (uploadMode.type === 'release') {
|
|
1250
|
+
uploadSourcemapsWithRelease({
|
|
1251
|
+
sentryCliPath,
|
|
1252
|
+
bundlePath,
|
|
1253
|
+
sourcemapPath,
|
|
1254
|
+
release: uploadMode.release,
|
|
1255
|
+
dist: uploadMode.dist,
|
|
1256
|
+
useStandaloneSourcemapsCommand,
|
|
1257
|
+
});
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
console.log((0, i18n_1.t)('uploadingSourcemap'));
|
|
1261
|
+
if (!useStandaloneSourcemapsCommand ||
|
|
1262
|
+
!supportsSentryDebugIdReference(sentryCliPath)) {
|
|
1263
|
+
const explicitRelease = resolveSentryReleaseFromValues(sentryOptions.sentryRelease, sentryOptions.sentryDist) ??
|
|
1264
|
+
resolveSentryReleaseFromValues(process.env.SENTRY_RELEASE, process.env.SENTRY_DIST);
|
|
1265
|
+
if (!explicitRelease) {
|
|
1266
|
+
throw new Error('[pakta/sentry] sentry-cli does not support Debug ID source map upload. ' +
|
|
1267
|
+
'Upgrade @sentry/cli, or set --sentry-release/--sentry-dist for legacy release matching.');
|
|
1268
|
+
}
|
|
1269
|
+
uploadSourcemapsWithRelease({
|
|
1270
|
+
sentryCliPath,
|
|
1271
|
+
bundlePath,
|
|
1272
|
+
sourcemapPath,
|
|
1273
|
+
release: explicitRelease.release,
|
|
1274
|
+
dist: explicitRelease.dist,
|
|
1275
|
+
useStandaloneSourcemapsCommand,
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
console.log(`[pakta/sentry] Using source map Debug ID: ${uploadMode.debugId}`);
|
|
1280
|
+
const debugIdResult = runSentryCli(buildSentrySourcemapsUploadArgs({
|
|
1281
|
+
sentryCliPath,
|
|
1282
|
+
bundlePath,
|
|
1283
|
+
sourcemapPath,
|
|
1284
|
+
debugIdReference: true,
|
|
1285
|
+
useStandaloneSourcemapsCommand,
|
|
1286
|
+
}));
|
|
1287
|
+
if (debugIdResult.status === 0 && !debugIdResult.error) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const explicitRelease = resolveSentryReleaseFromValues(sentryOptions.sentryRelease, sentryOptions.sentryDist) ??
|
|
1291
|
+
resolveSentryReleaseFromValues(process.env.SENTRY_RELEASE, process.env.SENTRY_DIST);
|
|
1292
|
+
if (!explicitRelease) {
|
|
1293
|
+
assertSuccessfulSyncProcess(debugIdResult, sentryCliPath);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
console.warn('[pakta/sentry] Debug ID source map upload failed; falling back to explicit release/dist upload.');
|
|
1297
|
+
uploadSourcemapsWithRelease({
|
|
1298
|
+
sentryCliPath,
|
|
1299
|
+
bundlePath,
|
|
1300
|
+
sourcemapPath,
|
|
1301
|
+
release: explicitRelease.release,
|
|
1302
|
+
dist: explicitRelease.dist,
|
|
1303
|
+
useStandaloneSourcemapsCommand,
|
|
1304
|
+
});
|
|
1305
|
+
}
|