pulse-updates 1.0.16 → 1.0.18

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/publish.mjs +121 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-updates",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "OTA updates for React Native - lightweight alternative to expo-updates",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -157,12 +157,15 @@ function getMimeType(ext) {
157
157
  */
158
158
  function findHermesc(platform) {
159
159
  const possiblePaths = [
160
- // iOS paths
160
+ // iOS paths (present once Pods are installed)
161
161
  'ios/Pods/hermes-engine/destroot/bin/hermesc',
162
+ // hermesc as shipped inside react-native itself (host-arch dirs vary by RN version)
162
163
  'node_modules/react-native/sdks/hermesc/osx-bin/hermesc',
163
164
  'node_modules/react-native/sdks/hermesc/linux64-bin/hermesc',
164
- // Android paths
165
+ 'node_modules/react-native/sdks/hermesc/win64-bin/hermesc.exe',
166
+ // Android paths (present once an Android build has run)
165
167
  'node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc',
168
+ 'node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/tools/hermesc/hermesc',
166
169
  ];
167
170
 
168
171
  for (const p of possiblePaths) {
@@ -172,12 +175,61 @@ function findHermesc(platform) {
172
175
  }
173
176
  }
174
177
 
175
- // Try to find via which
178
+ // Try to find via PATH
176
179
  try {
177
- return execSync('which hermesc', { encoding: 'utf8' }).trim();
180
+ const viaPath = execSync('command -v hermesc', { encoding: 'utf8' }).trim();
181
+ if (viaPath) return viaPath;
178
182
  } catch {
179
- return null;
183
+ /* not on PATH */
184
+ }
185
+ return null;
186
+ }
187
+
188
+ /**
189
+ * Locate hermesc, and if it isn't there yet, try to materialize it.
190
+ * Only used when the caller has REQUIRED Hermes (config.requireHermes / --require-hermes).
191
+ * A React Native app may only carry hermesc inside the installed CocoaPods (it
192
+ * does not always ship in node_modules for every RN version), so on macOS a
193
+ * missing compiler often means "Pods aren't installed" — which we fix by running
194
+ * pod-install once, then re-checking. If it still can't be found we throw, rather
195
+ * than silently shipping a plain-JS OTA bundle (readable source over the air).
196
+ * Apps that intentionally run without Hermes must NOT set requireHermes — they get
197
+ * the default best-effort path (compile if hermesc is present, plain JS otherwise).
198
+ */
199
+ function ensureHermesc(platform) {
200
+ let hermesc = findHermesc(platform);
201
+ if (hermesc) return hermesc;
202
+
203
+ const onMac = process.platform === 'darwin';
204
+ const hasPodfile = fs.existsSync(path.resolve('ios/Podfile'));
205
+ if (onMac && hasPodfile) {
206
+ logWarning('Hermes compiler not found — installing iOS Pods to obtain it (one-shot)...');
207
+ try {
208
+ // Prefer a repo pod-install helper if the app provides one, else npx pod-install.
209
+ const repoHelper = path.resolve('scripts/pod-install-if-mac.js');
210
+ const cmd = fs.existsSync(repoHelper)
211
+ ? `node ${repoHelper}`
212
+ : 'npx pod-install ios';
213
+ execSync(cmd, { stdio: 'inherit' });
214
+ } catch (error) {
215
+ throw new Error(
216
+ `Hermes compiler not found and automatic pod install failed (${error.message}). ` +
217
+ 'Run `npx pod-install ios` (or `cd ios && pod install`) and publish again.'
218
+ );
219
+ }
220
+ hermesc = findHermesc(platform);
221
+ }
222
+
223
+ if (!hermesc) {
224
+ throw new Error(
225
+ 'Hermes compiler (hermesc) could not be located. Pulse publishes Hermes bytecode only — ' +
226
+ 'a plain-JS OTA bundle must never be shipped. Install it before publishing:\n' +
227
+ ' • macOS: `npx pod-install ios` (materializes ios/Pods/hermes-engine/.../hermesc)\n' +
228
+ ' • or ensure react-native ships hermesc under node_modules/react-native/sdks/hermesc/\n' +
229
+ ' • or put hermesc on PATH.'
230
+ );
180
231
  }
232
+ return hermesc;
181
233
  }
182
234
 
183
235
  /**
@@ -210,11 +262,26 @@ function createBundle(platform, bundleDir, entryFile = 'index.ts') {
210
262
  }
211
263
 
212
264
  /**
213
- * Compile bundle with Hermes
265
+ * Compile bundle with Hermes.
266
+ *
267
+ * Default (best-effort): if hermesc is found, compile to bytecode; if not, warn and
268
+ * ship the plain JS bundle — this is correct for apps that don't run Hermes (JSC,
269
+ * or an intentionally plain-JS runtime), which is why it stays the default.
270
+ *
271
+ * With options.requireHermes (config.requireHermes / --require-hermes): the app has
272
+ * declared it runs on Hermes, so a plain-JS OTA would be both readable source and a
273
+ * runtime mismatch. In that mode we locate-or-materialize hermesc (throwing if we
274
+ * can't), pass -fstrip-function-names to shrink the string table, and assert the
275
+ * output is bytecode before returning. eSound and Lyra run in this mode.
214
276
  */
215
- function compileWithHermes(bundlePath, platform) {
216
- const hermesc = findHermesc(platform);
277
+ function compileWithHermes(bundlePath, platform, options = {}) {
278
+ const requireHermes = !!options.requireHermes;
279
+
280
+ // In required mode, ensureHermesc throws if it can't obtain a compiler.
281
+ // Otherwise fall back to best-effort discovery.
282
+ const hermesc = requireHermes ? ensureHermesc(platform) : findHermesc(platform);
217
283
  if (!hermesc) {
284
+ // Only reachable when Hermes is NOT required.
218
285
  logWarning('Hermes compiler not found, using plain JS bundle');
219
286
  return bundlePath;
220
287
  }
@@ -223,33 +290,49 @@ function compileWithHermes(bundlePath, platform) {
223
290
 
224
291
  log(` Compiling with Hermes...`, colors.dim);
225
292
 
226
- try {
227
- // Use spawnSync with larger buffer to avoid ENOBUFS error
228
- const result = spawnSync(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath], {
229
- stdio: ['pipe', 'pipe', 'pipe'],
230
- maxBuffer: 100 * 1024 * 1024, // 100MB buffer
231
- encoding: 'utf8',
232
- });
233
-
234
- if (result.error) {
235
- throw result.error;
236
- }
293
+ // -emit-binary: Hermes bytecode.
294
+ // -fstrip-function-names (required mode only): drop JS function names from the
295
+ // bytecode string table (smaller bundle, less readable). Trade-off: JS function
296
+ // names disappear from crash stack traces, so it stays opt-in via requireHermes.
297
+ // Does NOT touch string literals.
298
+ const hermesArgs = ['-emit-binary'];
299
+ if (requireHermes) hermesArgs.push('-fstrip-function-names');
300
+ hermesArgs.push('-out', hbcPath, bundlePath);
301
+
302
+ // Use spawnSync with larger buffer to avoid ENOBUFS error
303
+ const result = spawnSync(hermesc, hermesArgs, {
304
+ stdio: ['pipe', 'pipe', 'pipe'],
305
+ maxBuffer: 100 * 1024 * 1024, // 100MB buffer
306
+ encoding: 'utf8',
307
+ });
237
308
 
238
- if (result.status !== 0) {
239
- const stderr = result.stderr || '';
240
- throw new Error(`Hermes exited with code ${result.status}: ${stderr.slice(0, 500)}`);
241
- }
309
+ if (result.error) {
310
+ if (requireHermes) throw result.error;
311
+ logWarning(`Hermes compilation failed: ${result.error.message}`);
312
+ return bundlePath;
313
+ }
242
314
 
243
- // Remove the plain JS bundle, keep only HBC
244
- fs.unlinkSync(bundlePath);
245
- // Rename .hbc to .bundle for compatibility
246
- const finalPath = bundlePath;
247
- fs.renameSync(hbcPath, finalPath);
248
- return finalPath;
249
- } catch (error) {
250
- logWarning(`Hermes compilation failed: ${error.message}`);
315
+ if (result.status !== 0) {
316
+ const stderr = result.stderr || '';
317
+ const message = `Hermes exited with code ${result.status}: ${stderr.slice(0, 500)}`;
318
+ if (requireHermes) throw new Error(message);
319
+ logWarning(`Hermes compilation failed: ${message}`);
251
320
  return bundlePath;
252
321
  }
322
+
323
+ // Remove the plain JS bundle, keep only HBC
324
+ fs.unlinkSync(bundlePath);
325
+ // Rename .hbc to .bundle for compatibility
326
+ const finalPath = bundlePath;
327
+ fs.renameSync(hbcPath, finalPath);
328
+
329
+ // In required mode, belt-and-suspenders: the launch asset MUST be Hermes bytecode.
330
+ if (requireHermes && !isHermesBytecode(finalPath)) {
331
+ throw new Error(
332
+ 'Post-compile check failed: launch bundle is not Hermes bytecode. Refusing to publish a plain-JS OTA bundle.'
333
+ );
334
+ }
335
+ return finalPath;
253
336
  }
254
337
 
255
338
  /**
@@ -656,6 +739,12 @@ function loadConfig(options) {
656
739
  skipBundle: options['skip-bundle'] || false,
657
740
  build: options.build || process.env.PULSE_BUILD || fileConfig.build || null,
658
741
  message: options.message || fileConfig.message || null,
742
+ // Opt-in: the app runs on Hermes, so a plain-JS OTA must never be shipped.
743
+ // Makes a missing/failing hermesc fatal (with a one-shot pod-install attempt on
744
+ // macOS), enables -fstrip-function-names, and asserts bytecode output.
745
+ // Leave unset for apps that intentionally run plain JS (best-effort default).
746
+ requireHermes: options['require-hermes'] || process.env.PULSE_REQUIRE_HERMES === '1' ||
747
+ fileConfig.requireHermes || false,
659
748
  };
660
749
  }
661
750
 
@@ -705,7 +794,7 @@ async function publish(options) {
705
794
 
706
795
  // Step 2: Compile with Hermes
707
796
  logStep('2/6', 'Compiling with Hermes...');
708
- compileWithHermes(bundlePath, config.platform);
797
+ compileWithHermes(bundlePath, config.platform, { requireHermes: config.requireHermes });
709
798
  logSuccess('Hermes compilation complete');
710
799
  } else {
711
800
  logStep('1/6', 'Skipping bundle creation (--skip-bundle)');