pulse-updates 1.0.17 → 1.0.19

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 +108 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-updates",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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",
@@ -187,12 +187,14 @@ function findHermesc(platform) {
187
187
 
188
188
  /**
189
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).
190
191
  * A React Native app may only carry hermesc inside the installed CocoaPods (it
191
192
  * does not always ship in node_modules for every RN version), so on macOS a
192
193
  * missing compiler often means "Pods aren't installed" — which we fix by running
193
- * pod-install once, then re-checking. Anything else is a hard stop: we NEVER
194
- * silently fall back to a plain-JS OTA bundle (that would ship readable source
195
- * over the air). Hermes is mandatory on every publish.
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).
196
198
  */
197
199
  function ensureHermesc(platform) {
198
200
  let hermesc = findHermesc(platform);
@@ -260,23 +262,42 @@ function createBundle(platform, bundleDir, entryFile = 'index.ts') {
260
262
  }
261
263
 
262
264
  /**
263
- * 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.
264
276
  */
265
- function compileWithHermes(bundlePath, platform) {
266
- // Mandatory: locate (and if needed materialize) hermesc. Throws rather than
267
- // degrading to a plain-JS bundle — see ensureHermesc.
268
- const hermesc = ensureHermesc(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);
283
+ if (!hermesc) {
284
+ // Only reachable when Hermes is NOT required.
285
+ logWarning('Hermes compiler not found, using plain JS bundle');
286
+ return bundlePath;
287
+ }
269
288
 
270
289
  const hbcPath = bundlePath.replace('.bundle', '.hbc');
271
290
 
272
291
  log(` Compiling with Hermes...`, colors.dim);
273
292
 
274
293
  // -emit-binary: Hermes bytecode.
275
- // -fstrip-function-names: drop JS function names from the string table (smaller
276
- // bundle, and function names are the biggest readability gift to anyone
277
- // inspecting the bytecode). Trade-off: JS function names disappear from crash
278
- // stack traces. Does NOT touch string literals.
279
- const hermesArgs = ['-emit-binary', '-fstrip-function-names', '-out', hbcPath, bundlePath];
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);
280
301
 
281
302
  // Use spawnSync with larger buffer to avoid ENOBUFS error
282
303
  const result = spawnSync(hermesc, hermesArgs, {
@@ -286,12 +307,17 @@ function compileWithHermes(bundlePath, platform) {
286
307
  });
287
308
 
288
309
  if (result.error) {
289
- throw result.error;
310
+ if (requireHermes) throw result.error;
311
+ logWarning(`Hermes compilation failed: ${result.error.message}`);
312
+ return bundlePath;
290
313
  }
291
314
 
292
315
  if (result.status !== 0) {
293
316
  const stderr = result.stderr || '';
294
- throw new Error(`Hermes exited with code ${result.status}: ${stderr.slice(0, 500)}`);
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}`);
320
+ return bundlePath;
295
321
  }
296
322
 
297
323
  // Remove the plain JS bundle, keep only HBC
@@ -300,9 +326,8 @@ function compileWithHermes(bundlePath, platform) {
300
326
  const finalPath = bundlePath;
301
327
  fs.renameSync(hbcPath, finalPath);
302
328
 
303
- // Belt-and-suspenders: the launch asset MUST be Hermes bytecode. If for any
304
- // reason it isn't, fail the publish instead of shipping readable JS OTA.
305
- if (!isHermesBytecode(finalPath)) {
329
+ // In required mode, belt-and-suspenders: the launch asset MUST be Hermes bytecode.
330
+ if (requireHermes && !isHermesBytecode(finalPath)) {
306
331
  throw new Error(
307
332
  'Post-compile check failed: launch bundle is not Hermes bytecode. Refusing to publish a plain-JS OTA bundle.'
308
333
  );
@@ -621,6 +646,43 @@ function detectRuntimeVersion(platform) {
621
646
  return null;
622
647
  }
623
648
 
649
+ /**
650
+ * Detect whether the app is configured to run on Hermes, from its native build
651
+ * config. Used to auto-resolve requireHermes when it isn't explicitly set: a Hermes
652
+ * app should never receive a plain-JS OTA, so detection turns enforcement on for it
653
+ * without the developer having to declare the flag. Returns true / false / null
654
+ * (null = couldn't determine → caller stays best-effort).
655
+ */
656
+ function detectHermesEnabled(platform) {
657
+ try {
658
+ if (platform === 'android') {
659
+ const gp = path.resolve('android/gradle.properties');
660
+ if (fs.existsSync(gp)) {
661
+ const m = fs.readFileSync(gp, 'utf8').match(/^\s*hermesEnabled\s*=\s*(\S+)/m);
662
+ if (m) return /^true$/i.test(m[1].trim());
663
+ }
664
+ } else if (platform === 'ios') {
665
+ const podfile = path.resolve('ios/Podfile');
666
+ if (fs.existsSync(podfile)) {
667
+ const txt = fs.readFileSync(podfile, 'utf8');
668
+ // ENV['USE_HERMES'] = '1' / USE_HERMES => true, or :hermes_enabled => true
669
+ const useHermes = txt.match(/USE_HERMES['"\]\s]*[=>]+\s*['"]?(\w+)/);
670
+ if (useHermes) return /^(1|true|yes)$/i.test(useHermes[1]);
671
+ const flag = txt.match(/:hermes_enabled\s*=>\s*(\w+)/);
672
+ if (flag) return /^true$/i.test(flag[1]);
673
+ }
674
+ // Fallback: the Pods lockfile lists hermes-engine when Hermes is on.
675
+ const lock = path.resolve('ios/Podfile.lock');
676
+ if (fs.existsSync(lock)) {
677
+ return /hermes-engine/.test(fs.readFileSync(lock, 'utf8'));
678
+ }
679
+ }
680
+ } catch {
681
+ /* detection is best-effort */
682
+ }
683
+ return null; // unknown
684
+ }
685
+
624
686
  /**
625
687
  * Extract base API URL from full manifest URL
626
688
  * e.g., "https://pulse.example.com/pulse/manifest/my-app" -> "https://pulse.example.com"
@@ -703,6 +765,27 @@ function loadConfig(options) {
703
765
  }
704
766
  }
705
767
 
768
+ // Resolve Hermes enforcement (tri-state): explicit true/false wins; when left
769
+ // unset, auto-detect from the app's native config — a Hermes app is enforced,
770
+ // anything else stays best-effort.
771
+ let requireHermes =
772
+ options['require-hermes'] ? true
773
+ : options['no-require-hermes'] ? false
774
+ : process.env.PULSE_REQUIRE_HERMES != null ? (process.env.PULSE_REQUIRE_HERMES === '1')
775
+ : fileConfig.requireHermes; // true | false | undefined
776
+ if (requireHermes === undefined) {
777
+ const detected = platform ? detectHermesEnabled(platform) : null;
778
+ requireHermes = detected === true;
779
+ log(
780
+ ` Auto-detected Hermes enforcement: ${
781
+ detected === true ? 'ON (app uses Hermes)'
782
+ : detected === false ? 'OFF (app not on Hermes)'
783
+ : 'OFF (could not determine — best-effort)'
784
+ }`,
785
+ colors.dim
786
+ );
787
+ }
788
+
706
789
  return {
707
790
  apiUrl,
708
791
  apiKey: options['api-key'] || process.env.PULSE_API_KEY || fileConfig.apiKey,
@@ -714,6 +797,12 @@ function loadConfig(options) {
714
797
  skipBundle: options['skip-bundle'] || false,
715
798
  build: options.build || process.env.PULSE_BUILD || fileConfig.build || null,
716
799
  message: options.message || fileConfig.message || null,
800
+ // Whether to enforce Hermes (resolved above): explicit config/flag/env, or
801
+ // auto-detected from the app's native build config when left unset. When on, a
802
+ // missing/failing hermesc is fatal (one-shot pod-install attempt on macOS),
803
+ // -fstrip-function-names is passed, and bytecode output is asserted — so a
804
+ // Hermes app can never ship a readable plain-JS OTA.
805
+ requireHermes,
717
806
  };
718
807
  }
719
808
 
@@ -763,7 +852,7 @@ async function publish(options) {
763
852
 
764
853
  // Step 2: Compile with Hermes
765
854
  logStep('2/6', 'Compiling with Hermes...');
766
- compileWithHermes(bundlePath, config.platform);
855
+ compileWithHermes(bundlePath, config.platform, { requireHermes: config.requireHermes });
767
856
  logSuccess('Hermes compilation complete');
768
857
  } else {
769
858
  logStep('1/6', 'Skipping bundle creation (--skip-bundle)');