hermes-test 1.0.5 → 1.1.1

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/README.md CHANGED
@@ -45,6 +45,26 @@ Micro benchmarks (Apple Silicon, no coverage):
45
45
  | 50 hook tests (renderHook + act) | **75ms** | 721ms | **10x** |
46
46
  | Trivial cold start | **4.6ms** | 1,486ms | **364x** |
47
47
 
48
+ ### V8 evaluation summary
49
+
50
+ We ran a full V8 evaluation on a large real-world Expo workload.
51
+
52
+ | Scenario | Hermes | V8 | Observation |
53
+ |---|---:|---:|---|
54
+ | Full run (no coverage) | ~3–5s | ~3s in best observed local run | Comparable in best case |
55
+ | Coverage run | ~7–8s | ~20–26s | Hermes clearly faster |
56
+ | Watch mode | Stable baseline | Improved but still experimental | Hermes better day-to-day DX |
57
+
58
+ **Why Hermes won overall:** bytecode-first startup path, better fit with current cache architecture, and significantly lower overhead in the current coverage pipeline.
59
+
60
+ **What bytecode-first means in this runner:**
61
+ 1. Bundle once with esbuild.
62
+ 2. Compile bundle to Hermes bytecode (`.hbc`) ahead of execution.
63
+ 3. Execute cached bytecode directly (`eval_bytes`) instead of parsing large JS text at runtime.
64
+ 4. Reuse `.hbc` from cache on subsequent runs.
65
+
66
+ Detailed notes: `.claude/references/v8-evaluation-summary.md`
67
+
48
68
  ---
49
69
 
50
70
  ## Quick start
@@ -41,6 +41,192 @@ if (typeof Object.fromEntries === 'undefined') {
41
41
  };
42
42
  }
43
43
 
44
+ // Intl NumberFormat fallback for Linux Hermes builds where locale is ignored.
45
+ // Detect broken behavior first so macOS/Android native Intl remains untouched.
46
+ (function () {
47
+ function pickLocale(locales) {
48
+ if (Array.isArray(locales) && locales.length > 0) return String(locales[0] || 'en-US');
49
+ if (typeof locales === 'string' && locales) return locales;
50
+ return 'en-US';
51
+ }
52
+
53
+ function normalizeDigits(style, options) {
54
+ var minDefault = 0;
55
+ var maxDefault = 3;
56
+ if (style === 'currency') {
57
+ minDefault = 2;
58
+ maxDefault = 2;
59
+ } else if (style === 'percent') {
60
+ minDefault = 0;
61
+ maxDefault = 0;
62
+ }
63
+ var minDigits = typeof options.minimumFractionDigits === 'number' ? options.minimumFractionDigits : minDefault;
64
+ var maxDigits = typeof options.maximumFractionDigits === 'number' ? options.maximumFractionDigits : maxDefault;
65
+ if (maxDigits < minDigits) maxDigits = minDigits;
66
+ return { min: minDigits, max: maxDigits };
67
+ }
68
+
69
+ function separatorsForLocale(locale) {
70
+ var lc = String(locale || 'en-US').toLowerCase();
71
+ if (lc.indexOf('da') === 0) return { group: '.', decimal: ',' };
72
+ if (lc.indexOf('de') === 0) return { group: '.', decimal: ',' };
73
+ if (lc.indexOf('fr') === 0) return { group: ' ', decimal: ',' };
74
+ return { group: ',', decimal: '.' };
75
+ }
76
+
77
+ function addGrouping(intPart, group) {
78
+ var out = '';
79
+ var count = 0;
80
+ for (var i = intPart.length - 1; i >= 0; i--) {
81
+ out = intPart[i] + out;
82
+ count++;
83
+ if (i > 0 && count % 3 === 0) out = group + out;
84
+ }
85
+ return out;
86
+ }
87
+
88
+ function trimFraction(fracPart, minDigits) {
89
+ while (fracPart.length > minDigits && fracPart[fracPart.length - 1] === '0') {
90
+ fracPart = fracPart.slice(0, -1);
91
+ }
92
+ return fracPart;
93
+ }
94
+
95
+ function formatNumberValue(value, locales, options) {
96
+ var num = Number(value);
97
+ if (!isFinite(num)) return String(num);
98
+ options = options || {};
99
+
100
+ var locale = pickLocale(locales);
101
+ var style = options.style || 'decimal';
102
+ var useGrouping = options.useGrouping !== false;
103
+ var digits = normalizeDigits(style, options);
104
+ var minDigits = digits.min;
105
+ var maxDigits = digits.max;
106
+
107
+ if (style === 'percent') num = num * 100;
108
+ var sign = num < 0 ? '-' : '';
109
+ var abs = Math.abs(num);
110
+
111
+ var fixed = abs.toFixed(maxDigits);
112
+ var parts = fixed.split('.');
113
+ var intPart = parts[0];
114
+ var fracPart = parts[1] || '';
115
+
116
+ if (maxDigits > minDigits) fracPart = trimFraction(fracPart, minDigits);
117
+
118
+ var sep = separatorsForLocale(locale);
119
+ if (useGrouping) intPart = addGrouping(intPart, sep.group);
120
+
121
+ var formatted = sign + intPart + (fracPart ? sep.decimal + fracPart : '');
122
+
123
+ if (style === 'percent') return formatted + '%';
124
+ if (style === 'currency' && options.currency) {
125
+ if (String(locale).toLowerCase().indexOf('en') === 0) return options.currency + ' ' + formatted;
126
+ return formatted + ' ' + options.currency;
127
+ }
128
+ return formatted;
129
+ }
130
+
131
+ function isBrokenIntlNumberFormatting() {
132
+ try {
133
+ var da = (1234.56).toLocaleString('da-DK');
134
+ var en = (1234.56).toLocaleString('en-US');
135
+ if (typeof da !== 'string' || typeof en !== 'string') return true;
136
+ if (da === en) return true;
137
+ if (da.indexOf('560000') !== -1 || en.indexOf('560000') !== -1) return true;
138
+ return false;
139
+ } catch (_e) {
140
+ return true;
141
+ }
142
+ }
143
+
144
+ if (!isBrokenIntlNumberFormatting()) return;
145
+
146
+ if (typeof globalThis.Intl === 'undefined') globalThis.Intl = {};
147
+
148
+ function NumberFormat(locales, options) {
149
+ if (!(this instanceof NumberFormat)) return new NumberFormat(locales, options);
150
+ this._locales = locales;
151
+ this._options = options || {};
152
+ this._boundFormat = null;
153
+ }
154
+ NumberFormat.supportedLocalesOf = function (locales) {
155
+ if (Array.isArray(locales)) return locales.map(String);
156
+ if (typeof locales === 'string') return [locales];
157
+ return [];
158
+ };
159
+ NumberFormat.prototype.format = function (value) {
160
+ return formatNumberValue(value, this._locales, this._options);
161
+ };
162
+ function getBoundFormat(instance) {
163
+ if (!instance._boundFormat) {
164
+ var self = instance;
165
+ instance._boundFormat = function (value) {
166
+ return formatNumberValue(value, self._locales, self._options);
167
+ };
168
+ }
169
+ return instance._boundFormat;
170
+ }
171
+ Object.defineProperty(NumberFormat.prototype, 'format', {
172
+ configurable: true,
173
+ enumerable: false,
174
+ get: function () {
175
+ return getBoundFormat(this);
176
+ },
177
+ });
178
+ NumberFormat.prototype.resolvedOptions = function () {
179
+ var style = this._options.style || 'decimal';
180
+ var digits = normalizeDigits(style, this._options);
181
+ return {
182
+ locale: pickLocale(this._locales),
183
+ style: style,
184
+ useGrouping: this._options.useGrouping !== false,
185
+ minimumFractionDigits: digits.min,
186
+ maximumFractionDigits: digits.max,
187
+ };
188
+ };
189
+
190
+ globalThis.Intl.NumberFormat = NumberFormat;
191
+ Number.prototype.toLocaleString = function (locales, options) {
192
+ return formatNumberValue(Number(this), locales, options);
193
+ };
194
+ })();
195
+
196
+ // Intl String locale casing fallback for Linux Hermes ICU stub behavior.
197
+ // Some Linux builds return placeholder values (e.g. "lowered"/"UPPERED")
198
+ // instead of transformed text. Keep native behavior when it works.
199
+ (function () {
200
+ function isBrokenIntlStringCasing() {
201
+ try {
202
+ var lower = 'AbC'.toLocaleLowerCase();
203
+ var upper = 'aBc'.toLocaleUpperCase();
204
+ if (typeof lower !== 'string' || typeof upper !== 'string') return true;
205
+ return lower !== 'abc' || upper !== 'ABC';
206
+ } catch (_e) {
207
+ return true;
208
+ }
209
+ }
210
+
211
+ if (!isBrokenIntlStringCasing()) return;
212
+
213
+ Object.defineProperty(String.prototype, 'toLocaleLowerCase', {
214
+ configurable: true,
215
+ writable: true,
216
+ value: function () {
217
+ return String(this).toLowerCase();
218
+ },
219
+ });
220
+
221
+ Object.defineProperty(String.prototype, 'toLocaleUpperCase', {
222
+ configurable: true,
223
+ writable: true,
224
+ value: function () {
225
+ return String(this).toUpperCase();
226
+ },
227
+ });
228
+ })();
229
+
44
230
  // crypto.getRandomValues — needed by uuid and other crypto-dependent libs
45
231
  if (typeof globalThis.crypto === 'undefined') {
46
232
  globalThis.crypto = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-test",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "26-64x faster than Jest. A test runner built for React Native and Expo. One esbuild pass, one process, zero Babel.",
5
5
  "main": "src/index.ts",
6
6
  "types": "index.d.ts",
@@ -54,9 +54,9 @@
54
54
  "react": ">=18"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@hermes-test/darwin-arm64": "1.0.5",
58
- "@hermes-test/darwin-x64": "1.0.5",
59
- "@hermes-test/linux-x64": "1.0.5"
57
+ "@hermes-test/darwin-arm64": "1.1.1",
58
+ "@hermes-test/darwin-x64": "1.1.1",
59
+ "@hermes-test/linux-x64": "1.1.1"
60
60
  },
61
61
  "files": [
62
62
  "src/",
package/src/polyfills.js CHANGED
@@ -41,6 +41,192 @@ if (typeof Object.fromEntries === 'undefined') {
41
41
  };
42
42
  }
43
43
 
44
+ // Intl NumberFormat fallback for Linux Hermes builds where locale is ignored.
45
+ // Detect broken behavior first so macOS/Android native Intl remains untouched.
46
+ (function () {
47
+ function pickLocale(locales) {
48
+ if (Array.isArray(locales) && locales.length > 0) return String(locales[0] || 'en-US');
49
+ if (typeof locales === 'string' && locales) return locales;
50
+ return 'en-US';
51
+ }
52
+
53
+ function normalizeDigits(style, options) {
54
+ var minDefault = 0;
55
+ var maxDefault = 3;
56
+ if (style === 'currency') {
57
+ minDefault = 2;
58
+ maxDefault = 2;
59
+ } else if (style === 'percent') {
60
+ minDefault = 0;
61
+ maxDefault = 0;
62
+ }
63
+ var minDigits = typeof options.minimumFractionDigits === 'number' ? options.minimumFractionDigits : minDefault;
64
+ var maxDigits = typeof options.maximumFractionDigits === 'number' ? options.maximumFractionDigits : maxDefault;
65
+ if (maxDigits < minDigits) maxDigits = minDigits;
66
+ return { min: minDigits, max: maxDigits };
67
+ }
68
+
69
+ function separatorsForLocale(locale) {
70
+ var lc = String(locale || 'en-US').toLowerCase();
71
+ if (lc.indexOf('da') === 0) return { group: '.', decimal: ',' };
72
+ if (lc.indexOf('de') === 0) return { group: '.', decimal: ',' };
73
+ if (lc.indexOf('fr') === 0) return { group: ' ', decimal: ',' };
74
+ return { group: ',', decimal: '.' };
75
+ }
76
+
77
+ function addGrouping(intPart, group) {
78
+ var out = '';
79
+ var count = 0;
80
+ for (var i = intPart.length - 1; i >= 0; i--) {
81
+ out = intPart[i] + out;
82
+ count++;
83
+ if (i > 0 && count % 3 === 0) out = group + out;
84
+ }
85
+ return out;
86
+ }
87
+
88
+ function trimFraction(fracPart, minDigits) {
89
+ while (fracPart.length > minDigits && fracPart[fracPart.length - 1] === '0') {
90
+ fracPart = fracPart.slice(0, -1);
91
+ }
92
+ return fracPart;
93
+ }
94
+
95
+ function formatNumberValue(value, locales, options) {
96
+ var num = Number(value);
97
+ if (!isFinite(num)) return String(num);
98
+ options = options || {};
99
+
100
+ var locale = pickLocale(locales);
101
+ var style = options.style || 'decimal';
102
+ var useGrouping = options.useGrouping !== false;
103
+ var digits = normalizeDigits(style, options);
104
+ var minDigits = digits.min;
105
+ var maxDigits = digits.max;
106
+
107
+ if (style === 'percent') num = num * 100;
108
+ var sign = num < 0 ? '-' : '';
109
+ var abs = Math.abs(num);
110
+
111
+ var fixed = abs.toFixed(maxDigits);
112
+ var parts = fixed.split('.');
113
+ var intPart = parts[0];
114
+ var fracPart = parts[1] || '';
115
+
116
+ if (maxDigits > minDigits) fracPart = trimFraction(fracPart, minDigits);
117
+
118
+ var sep = separatorsForLocale(locale);
119
+ if (useGrouping) intPart = addGrouping(intPart, sep.group);
120
+
121
+ var formatted = sign + intPart + (fracPart ? sep.decimal + fracPart : '');
122
+
123
+ if (style === 'percent') return formatted + '%';
124
+ if (style === 'currency' && options.currency) {
125
+ if (String(locale).toLowerCase().indexOf('en') === 0) return options.currency + ' ' + formatted;
126
+ return formatted + ' ' + options.currency;
127
+ }
128
+ return formatted;
129
+ }
130
+
131
+ function isBrokenIntlNumberFormatting() {
132
+ try {
133
+ var da = (1234.56).toLocaleString('da-DK');
134
+ var en = (1234.56).toLocaleString('en-US');
135
+ if (typeof da !== 'string' || typeof en !== 'string') return true;
136
+ if (da === en) return true;
137
+ if (da.indexOf('560000') !== -1 || en.indexOf('560000') !== -1) return true;
138
+ return false;
139
+ } catch (_e) {
140
+ return true;
141
+ }
142
+ }
143
+
144
+ if (!isBrokenIntlNumberFormatting()) return;
145
+
146
+ if (typeof globalThis.Intl === 'undefined') globalThis.Intl = {};
147
+
148
+ function NumberFormat(locales, options) {
149
+ if (!(this instanceof NumberFormat)) return new NumberFormat(locales, options);
150
+ this._locales = locales;
151
+ this._options = options || {};
152
+ this._boundFormat = null;
153
+ }
154
+ NumberFormat.supportedLocalesOf = function (locales) {
155
+ if (Array.isArray(locales)) return locales.map(String);
156
+ if (typeof locales === 'string') return [locales];
157
+ return [];
158
+ };
159
+ NumberFormat.prototype.format = function (value) {
160
+ return formatNumberValue(value, this._locales, this._options);
161
+ };
162
+ function getBoundFormat(instance) {
163
+ if (!instance._boundFormat) {
164
+ var self = instance;
165
+ instance._boundFormat = function (value) {
166
+ return formatNumberValue(value, self._locales, self._options);
167
+ };
168
+ }
169
+ return instance._boundFormat;
170
+ }
171
+ Object.defineProperty(NumberFormat.prototype, 'format', {
172
+ configurable: true,
173
+ enumerable: false,
174
+ get: function () {
175
+ return getBoundFormat(this);
176
+ },
177
+ });
178
+ NumberFormat.prototype.resolvedOptions = function () {
179
+ var style = this._options.style || 'decimal';
180
+ var digits = normalizeDigits(style, this._options);
181
+ return {
182
+ locale: pickLocale(this._locales),
183
+ style: style,
184
+ useGrouping: this._options.useGrouping !== false,
185
+ minimumFractionDigits: digits.min,
186
+ maximumFractionDigits: digits.max,
187
+ };
188
+ };
189
+
190
+ globalThis.Intl.NumberFormat = NumberFormat;
191
+ Number.prototype.toLocaleString = function (locales, options) {
192
+ return formatNumberValue(Number(this), locales, options);
193
+ };
194
+ })();
195
+
196
+ // Intl String locale casing fallback for Linux Hermes ICU stub behavior.
197
+ // Some Linux builds return placeholder values (e.g. "lowered"/"UPPERED")
198
+ // instead of transformed text. Keep native behavior when it works.
199
+ (function () {
200
+ function isBrokenIntlStringCasing() {
201
+ try {
202
+ var lower = 'AbC'.toLocaleLowerCase();
203
+ var upper = 'aBc'.toLocaleUpperCase();
204
+ if (typeof lower !== 'string' || typeof upper !== 'string') return true;
205
+ return lower !== 'abc' || upper !== 'ABC';
206
+ } catch (_e) {
207
+ return true;
208
+ }
209
+ }
210
+
211
+ if (!isBrokenIntlStringCasing()) return;
212
+
213
+ Object.defineProperty(String.prototype, 'toLocaleLowerCase', {
214
+ configurable: true,
215
+ writable: true,
216
+ value: function () {
217
+ return String(this).toLowerCase();
218
+ },
219
+ });
220
+
221
+ Object.defineProperty(String.prototype, 'toLocaleUpperCase', {
222
+ configurable: true,
223
+ writable: true,
224
+ value: function () {
225
+ return String(this).toUpperCase();
226
+ },
227
+ });
228
+ })();
229
+
44
230
  // crypto.getRandomValues — needed by uuid and other crypto-dependent libs
45
231
  if (typeof globalThis.crypto === 'undefined') {
46
232
  globalThis.crypto = {};