UNPKG

@rechunk/core

Version:

Core functionality for ReChunk dynamic code loading and chunk management in React Native

211 lines (180 loc) 6.44 kB
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), }; } }