Skip to content

Commit b8a98a4

Browse files
committed
feat(hermes-base): support native package fallback
1 parent 17ca6a1 commit b8a98a4

2 files changed

Lines changed: 49 additions & 11 deletions

File tree

src/utils/hermes-base.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export interface HermesBaseSelection {
2828
bundleHash: string;
2929
/** server version id when the base came from the server */
3030
versionId?: number;
31-
/** server object key of the base ppk when known */
31+
/** server object key of the base artifact when known */
3232
hash?: string;
33-
source: 'cache' | 'download' | 'local' | 'latest-version';
33+
source: 'cache' | 'download' | 'local' | 'latest-version' | 'native-package';
3434
}
3535

3636
/** Metadata attached to version/create so the server can track the chain. */
@@ -397,8 +397,9 @@ export async function downloadToFile(
397397
// ---------------------------------------------------------------------------
398398

399399
export interface HermesBaseServerRecord {
400-
versionId: number;
400+
versionId?: number | null;
401401
hash: string;
402+
artifactType?: 'ppk' | 'apk' | 'ipa' | 'app';
402403
bundleHash?: string | null;
403404
bytecodeVersion?: number | null;
404405
url: string;
@@ -412,8 +413,8 @@ export interface ResolveHermesBaseParams {
412413
cacheMaxMb?: number;
413414
/**
414415
* server lookup; returns null when the app has no usable base. The server
415-
* itself falls back to the newest version when the epoch is still unknown
416-
* (its bytecodeVersion is then null and verified here after download).
416+
* itself falls back to the newest legacy version, then the newest native
417+
* package (bytecodeVersion is null and verified here after download).
417418
*/
418419
fetchBase: (
419420
appId: string,
@@ -480,7 +481,11 @@ export async function resolveHermesBase(
480481
return null;
481482
}
482483
const source: HermesBaseSelection['source'] =
483-
record?.bytecodeVersion == null ? 'latest-version' : 'download';
484+
record?.artifactType && record.artifactType !== 'ppk'
485+
? 'native-package'
486+
: record?.bytecodeVersion == null
487+
? 'latest-version'
488+
: 'download';
484489
if (!record) {
485490
log(t('hermesBaseNone', { reason: 'no published version yet' }));
486491
return null;
@@ -502,15 +507,15 @@ export async function resolveHermesBase(
502507
if (hit) {
503508
log(
504509
t('hermesBaseUsing', {
505-
source: `cache ${record.bundleHash.slice(0, 12)} (version ${record.versionId})`,
510+
source: `cache ${record.bundleHash.slice(0, 12)}${record.versionId == null ? '' : ` (version ${record.versionId})`}`,
506511
version: bytecodeVersion,
507512
}),
508513
);
509514
return {
510515
path: hit,
511516
bytecodeVersion,
512517
bundleHash: record.bundleHash,
513-
versionId: record.versionId,
518+
versionId: record.versionId ?? undefined,
514519
hash: record.hash,
515520
source: 'cache',
516521
};
@@ -520,9 +525,14 @@ export async function resolveHermesBase(
520525
for (let attempt = 1; attempt <= 2; attempt++) {
521526
const dir = tmpDir();
522527
await fs.ensureDir(dir);
528+
const artifactType = ['ppk', 'apk', 'ipa', 'app'].includes(
529+
record.artifactType ?? '',
530+
)
531+
? record.artifactType
532+
: 'ppk';
523533
const archive = path.join(
524534
dir,
525-
`base-${process.pid}-${Date.now()}-${attempt}.ppk`,
535+
`base-${process.pid}-${Date.now()}-${attempt}.${artifactType}`,
526536
);
527537
try {
528538
log(t('hermesBaseDownloading', { url: record.url }));
@@ -548,15 +558,18 @@ export async function resolveHermesBase(
548558
const cached = await cachePut(bundle, params.cacheMaxMb);
549559
log(
550560
t('hermesBaseUsing', {
551-
source: `version ${record.versionId} ${record.hash.slice(0, 8)}`,
561+
source:
562+
record.versionId == null
563+
? `native package ${record.hash.slice(0, 8)}`
564+
: `version ${record.versionId} ${record.hash.slice(0, 8)}`,
552565
version: bytecodeVersion,
553566
}),
554567
);
555568
return {
556569
path: cached.path,
557570
bytecodeVersion,
558571
bundleHash: cached.bundleHash,
559-
versionId: record.versionId,
572+
versionId: record.versionId ?? undefined,
560573
hash: record.hash,
561574
source,
562575
};

tests/hermes-base.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,16 @@ describe('resolveHermesBase', () => {
283283
const bundleHash = sha256Hex(bundle);
284284
const ppk = path.join(dir, 'server.ppk');
285285
await writeZip(ppk, { 'main.jsbundle': bundle });
286+
const nativeBundle = fakeHbc(98, 'native');
287+
const apk = path.join(dir, 'native.apk');
288+
await writeZip(apk, { 'assets/index.android.bundle': nativeBundle });
286289
const server = Bun.serve({
287290
port: 0,
288291
fetch: async (req) => {
289292
if (new URL(req.url).pathname === '/good.ppk')
290293
return new Response(Bun.file(ppk));
294+
if (new URL(req.url).pathname === '/native.apk')
295+
return new Response(Bun.file(apk));
291296
return new Response('nope', { status: 404 });
292297
},
293298
});
@@ -335,6 +340,26 @@ describe('resolveHermesBase', () => {
335340
fetchBase: async () => legacy,
336341
});
337342
expect(fromLatest?.source).toBe('latest-version');
343+
await cleanCache();
344+
const fromNativePackage = await resolveHermesBase({
345+
...common,
346+
option: 'auto',
347+
appId: '1',
348+
fetchBase: async () => ({
349+
versionId: null,
350+
hash: 'nativekey',
351+
artifactType: 'apk',
352+
bundleHash: sha256Hex(nativeBundle),
353+
bytecodeVersion: null,
354+
url: `http://127.0.0.1:${server.port}/native.apk`,
355+
}),
356+
});
357+
expect(fromNativePackage?.source).toBe('native-package');
358+
expect(fromNativePackage?.versionId).toBeUndefined();
359+
expect(fromNativePackage?.hash).toBe('nativekey');
360+
expect(
361+
fs.readFileSync(fromNativePackage!.path).equals(nativeBundle),
362+
).toBe(true);
338363
// server has a different epoch → no base
339364
expect(
340365
await resolveHermesBase({

0 commit comments

Comments
 (0)