Version: v5

ScriptManager

The ScriptManager is a low-level utility for managing script resolution, downloading, and execution in React Native applications. It's particularly useful when working with code splitting, dynamic imports, and Module Federation.

Why is it called ScriptManager?

It can be used to download, manage and execute external (either local or remote) JavaScript code.

Usage

import { ScriptManager } from "@callstack/repack/client";

ScriptManager.shared.addResolver(async (scriptId, caller) => {
  if (__DEV__) {
    return {
      url: Script.getDevServerURL(scriptId),
      cache: false,
    };
  }

  return {
    url: Script.getRemoteURL(`https://mycdn.example/assets/${scriptId}`),
  };
});

// Example usage with React.lazy and dynamic imports
const TeacherModule = React.lazy(() => import("./Teacher.js"));
const StudentModule = React.lazy(() => import("./Student.js"));

export function App({ role }) {
  if (role === "teacher") {
    return <TeacherModule />;
  }

  return <StudentModule />;
}

API Reference

ScriptManager.shared

The globally shared instance of ScriptManager. You should always use this instead of creating new instances.

  • Type: ScriptManager

addResolver

Adds a new script locator resolver to handle script resolution.

  • Type: addResolver(resolver: ScriptLocatorResolver, options?: ResolverOptions): void
  • Parameters:
    • resolver: Async function that resolves script location data
    • options: Configuration options for the resolver
      • priority: Priority of the resolver (default: 2)
      • key: Unique key to identify the resolver

removeResolver

Removes a previously added resolver.

  • Type: removeResolver(resolver: ScriptLocatorResolver | string): boolean
  • Parameters:
    • resolver: The resolver function or its unique key to remove
  • Returns: true if resolver was found and removed, false otherwise

removeAllResolvers

Removes all previously added resolvers.

  • Type: removeAllResolvers(): void

setStorage

Sets a storage backend for caching resolved script locator data.

  • Type: setStorage(storage: StorageApi): void
  • Parameters:
    • storage: Storage API implementation with getItem and setItem methods

loadScript

Resolves, downloads, and executes a script.

  • Type: loadScript(scriptId: string, caller?: string, webpackContext?: any, referenceUrl?: string): Promise<void>
  • Parameters:
    • scriptId: Id of the script to load
    • caller: Name of the calling script (optional)
    • webpackContext: Webpack context (optional)
    • referenceUrl: Reference URL for resolution (optional)

prefetchScript

Downloads a script without executing it.

  • Type: prefetchScript(scriptId: string, caller?: string, webpackContext?: any, referenceUrl?: string): Promise<void>
  • Parameters:
    • scriptId: Id of the script to prefetch
    • caller: Name of the calling script (optional)
    • webpackContext: Webpack context (optional)
    • referenceUrl: Reference URL for resolution (optional)

invalidateScripts

Clears cache and removes downloaded files for given scripts.

  • Type: invalidateScripts(scriptIds?: string[]): Promise<string[]>
  • Parameters:
    • scriptIds: Array of script ids to invalidate (optional)
  • Returns: Promise resolving to array of invalidated script ids

Events

The ScriptManager extends EventEmitter and provides a comprehensive event system for monitoring and reacting to script loading lifecycle events. Events can be used to track script resolution, loading progress, and handle errors.

Available Events

The following events are emitted by ScriptManager:

Event NameDescription
resolvingEmitted when script resolution begins
resolvedEmitted when script resolution succeeds
prefetchingEmitted when script prefetching begins
loadingEmitted when script loading begins
loadedEmitted when script loading succeeds
errorEmitted when an error occurs during script resolution or loading
invalidatedEmitted when scripts are invalidated from cache

Subscribing to Events

import { ScriptManager } from "@callstack/repack/client";

// Listen for script loading events
ScriptManager.shared.on("loading", (script) => {
  console.log(`Loading script: ${script.scriptId}`);
});

ScriptManager.shared.on("loaded", (script) => {
  console.log(`Successfully loaded script: ${script.scriptId}`);
});

ScriptManager.shared.on("error", (error) => {
  console.error("Script loading failed:", error);
});

Hooks

The ScriptManager provides a hook system that allows developers to intercept and customize the script loading process at various stages. The hooks provide fine-grained control over script resolution and loading, enabling advanced use cases like custom caching strategies, script transformation, and error handling.

Available Hooks

Resolution Hooks

Hook NameDescription
beforeResolveCalled before script resolution begins, allows you to modify the arguments
resolveCustomise / override the script resolution process
afterResolveModify the resolved script locator after resolution succeeds
errorResolveProvide a fallback for resolution request when normal resolution fails

Loading Hooks

Hook NameDescription
beforeLoadCalled before script loading begins, allows you to modify the arguments
loadCustomise / override the script resolution process
afterLoadReact to successful script loading event
errorLoadProvide a fallback for a script when normal loading procedure fails

Using hooks

ScriptManager.shared.hooks.beforeResolve(async (args) => {
  console.debug("ScriptManager.shared.hooks.beforeResolve", args);
  return args;
});

ScriptManager.shared.hooks.resolve(async (args) => {
  console.debug("ScriptManager.shared.hooks.resolve", args);
  const { scriptId, caller, referenceUrl } = args.options;
  for (const [, , resolve] of args.resolvers) {
    const locator = await resolve(scriptId, caller, referenceUrl);
    if (locator) return locator;
  }
});

ScriptManager.shared.hooks.afterResolve(async (args) => {
  console.debug("ScriptManager.shared.hooks.afterResolve", args);
  return args;
});

ScriptManager.shared.hooks.beforeLoad(async (args) => {
  console.debug("ScriptManager.shared.hooks.beforeLoad", args);
  return args;
});

ScriptManager.shared.hooks.load(async (args) => {
  console.debug("ScriptManager.shared.hooks.load", args);
  await args.loadScript();
  return true;
});

ScriptManager.shared.hooks.afterLoad(async (args) => {
  console.debug("ScriptManager.shared.hooks.afterLoad", args);
  return args;
});

Advanced Usage

Custom Resolver with Retry Logic

import { ScriptManager } from "@callstack/repack/client";

ScriptManager.shared.addResolver(async (scriptId) => {
  return {
    url: `https://mycdn.example/assets/${scriptId}`,
    retry: 3, // Number of retry attempts
    retryDelay: 1000, // Delay between retries in milliseconds
    headers: {
      Authorization: "Bearer token",
    },
  };
});

Enabling caching through AsyncStorage

import { ScriptManager } from "@callstack/repack/client";
import AsyncStorage from "@react-native/async-storage";

ScriptManager.shared.setStorage({
  getItem: (key) => AsyncStorage.getItem(key),
  setItem: (key, value) => AsyncStorage.setItem(key, value),
});

Using per-script caching strategy

ScriptManager.shared.hooks.afterResolve(async (args) => {
  const { locator, options } = args;
  // Implement custom caching logic
  if (shouldCache(locator)) {
    locator.cache = true;
  }
  return args;
});

Override script locator URL after resolution

ScriptManager.shared.hooks.afterResolve(async (args) => {
  const { locator, options } = args;

  // Transform locator URL after it's resolved
  locator.url = transformUrl(locator.url);

  return args;
});

Adding fallback for failed loading of a script

ScriptManager.shared.hooks.errorLoad(async (args) => {
  const { error, options } = args;
  // Implement custom error handling
  if (isRecoverableError(error)) {
    await retryLoading(options);
    return true;
  }
  return false;
});