@rechunk/core
Version:
Core functionality for ReChunk dynamic code loading and chunk management in React Native
211 lines (180 loc) • 6.44 kB
text/typescript
import {evalx} from '@metro-requirex/react-native';
import {
type Chunk,
ChunksApi,
Configuration as ReChunkApiConfiguration,
} from '@rechunk/api-client';
import type React from 'react';
import {TinyEmitter} from 'tiny-emitter';
import invariant from 'tiny-invariant';
import warning from 'tiny-warning';
import {createIntegrityChecker} from './jws';
import type {Configuration, DeepRequired, ResolverFunction} from './types';
/**
* Manager class for handling chunk imports and caching.
*/
export class ChunkManager extends TinyEmitter {
/**
* Represents a static instance of the ChunkManager class.
* This static instance allows access to the ChunkManager functionality without the need to create new instances.
* @type {ChunkManager}
* @protected
*/
protected static instance: ChunkManager;
/**
* Cache to store imported chunks.
* This cache improves performance by storing previously imported chunks for future use.
* @type {Record<string, React.ComponentType<any>>}
* @protected
*/
protected cache: Record<string, React.ComponentType> = {};
/**
* An instance of the ReChunkApi class that is used to make requests to the API.
*
* @type {ReChunkApi}
* @protected
*/
protected request: ChunksApi = new ChunksApi(
new ReChunkApiConfiguration({basePath: process.env.__RECHUNK_HOST__}),
);
/**
* Resolver function used to resolve chunk imports.
* This function is responsible for dynamically loading and resolving imported chunks.
* @type {ResolverFunction}
* @protected
*/
protected resolver: ResolverFunction = async chunkId => {
try {
if (!chunkId || typeof chunkId !== 'string') {
throw new Error('[ReChunk]: Invalid chunkId provided');
}
const response = await this.request.getChunkById(
{
projectId: process.env.__RECHUNK_PROJECT__ as string,
chunkId,
},
{
headers: {
Authorization: `Basic ${btoa(
`${process.env.__RECHUNK_PROJECT__}:${process.env.__RECHUNK_READ_KEY__}`,
)}`,
},
},
);
if (!response.data || !response.token) {
throw new Error(`[ReChunk]: Failed to fetch chunk with ID ${chunkId}`);
}
return response as DeepRequired<Chunk>;
} catch (error: any) {
throw new Error(`[ReChunk]: Failed to fetch chunk: ${error.message}`);
}
};
/**
* Flag indicating whether verification is enabled.
* @type {boolean}
* @protected
*/
protected verify: boolean = true;
/**
* The public key used to verify function.
* @type {string}
* @protected
*/
protected publicKey: string = process.env.__RECHUNK_PUBLIC_KEY__ as string;
/**
* Get the shared instance of ChunkManager.
* @returns {ChunkManager} The shared instance of ChunkManager.
*/
static get shared(): ChunkManager {
if (!ChunkManager.instance) {
ChunkManager.instance = new ChunkManager();
}
return ChunkManager.instance;
}
/**
* Creates an instance of ChunkManager.
* @param {Object} nativeChunkManager - Native chunk manager module.
* @throws {Error} Throws error if instance is already created or if native chunk manager module is not found.
*/
protected constructor() {
super();
// Ensure only one instance of ChunkManager is created
invariant(
!ChunkManager.instance,
'[ReChunk]: ChunkManager was already instantiated. Use ChunkManager.shared instead.',
);
}
/**
* Converts chunk string to a JavaScript component.
* @param {string} chunkId - The chunk identifier.
* @param {string} chunk - The chunk string.
* @returns {*} The JavaScript component generated from the chunk.
*/
protected chunkToComponent(
chunkId: string,
chunk: string,
): React.ComponentType {
const exports = {};
const module = {exports};
const Component = evalx<React.ComponentType>(chunk);
// Add chunkId and chunk to cache
this.cache[chunkId] = Component;
// Notifies listeners that a chunk is available, facilitating communication
// for handling edge cases or asynchronous dependencies.
this.emit(chunkId);
return Component;
}
/**
* Adds configuration settings to the ChunkManager instance.
* This method sets the public key, resolver function, verification flag, and global object for the ChunkManager instance.
* @param {Configuration} config - The configuration object for ChunkManager.
* @param {boolean} [config.verify] - Flag indicating whether verification is enabled.
* @param {CustomRequire} [config.global] - Object representing protected global variables and functions.
* @param {ResolverFunction} [config.resolver] - The resolver function used to resolve chunk imports.
* @param {ResolverFunction} [config.publicKey] - The publicKey for ChunkManager configuration.
*/
addConfiguration({resolver, verify, publicKey}: Configuration) {
if (resolver) {
// Set the resolver function
this.resolver = resolver;
}
if (verify !== undefined) {
// Issue a warning if verification is turned off
warning(verify, '[ReChunk]: verification is off; chunks are insecure.');
// Set the verification flag
this.verify = verify;
}
if (publicKey) {
// Set the publicKey
this.publicKey = publicKey;
}
}
/**
* Imports a chunk asynchronously and returns the corresponding JavaScript component.
* @param {string} chunkId - The ID of the chunk to import.
* @returns {Promise<*>} A promise resolving to the JavaScript component imported from the chunk.
*/
async importChunk(chunkId: string): Promise<{default: React.ComponentType}> {
// If chunk is already cached, return the cached component
if (this.cache[chunkId]) {
return {default: this.cache[chunkId] as React.ComponentType};
}
// Resolve the chunk
const chunk = await this.resolver(chunkId);
const integrityChecker = createIntegrityChecker(
'sha256',
'RS256',
this.publicKey,
);
if (this.verify) {
const verified = integrityChecker.verify(chunk.data, chunk.token);
if (!verified) {
throw new Error('cannot verify hash');
}
return {default: this.chunkToComponent(chunkId, chunk.data)};
}
return {
default: this.chunkToComponent(chunkId, chunk.data),
};
}
}