npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iop362/framework

v1.0.1

Published

White-label React Native framework — shared modules, theming, and feature flags

Readme

@org/framework

Expo SDK React Native TypeScript License

White-label React Native framework — shared modules, theming, feature flags, and navigation for multiple branded apps from a single codebase.


Table of Contents

  1. Overview
  2. Monorepo Structure
  3. Architecture
  4. Quick Start
  5. FrameworkConfig Reference
  6. Theme Tokens
  7. String Tokens
  8. Callbacks Reference
  9. Feature Flags
  10. Module System
  11. Provider Hooks
  12. Peer Dependencies
  13. Development Setup
  14. Client App Examples
  15. Scripts
  16. Tech Stack

Overview

@org/framework is an internal React Native package that powers multiple white-label consumer apps. Each app is a thin shell — it provides a single FrameworkConfig object and the framework handles everything else: theming, navigation, feature-flagged modules, localised strings, and injectable business logic.

How it works:

// apps/client-a/app/index.tsx
import { FrameworkApp } from '@org/framework';
import { appConfig } from '../config/appConfig';

export default function App() {
  return <FrameworkApp config={appConfig} />;
}

That one line boots the entire app. All customisation — brand colours, copy, analytics callbacks, feature access — is expressed through appConfig.

What you get out of the box:

  • Bottom-tab navigation with remotely toggled modules
  • Offline-first feature flag caching (AsyncStorage)
  • Deep-merged theme tokens (override only what you need)
  • Localised/branded string resources
  • Injectable callbacks for analytics, formatting, and auth events
  • TypeScript-strict public API

Monorepo Structure

ReactNative/
├── framework/              ← @org/framework  (this repo)
│   ├── src/
│   │   ├── index.ts        ← Public API surface
│   │   ├── FrameworkApp.tsx
│   │   ├── config/         ← FrameworkConfig types
│   │   ├── theme/          ← ThemeProvider + defaults
│   │   ├── strings/        ← StringProvider + defaults
│   │   ├── callbacks/      ← CallbackProvider + defaults
│   │   ├── flags/          ← FeatureFlagProvider + types
│   │   ├── modules/        ← registry + home/profile/settings/payments
│   │   ├── navigation/     ← RootNavigator + LoadingScreen
│   │   └── utils/          ← deepMerge
│   └── package.json
│
└── apps/
    ├── client-a/           ← Red brand  (com.clienta.app)
    │   ├── app/            ← expo-router routes
    │   └── config/         ← appConfig.ts (theme, strings, callbacks)
    │
    └── client-b/           ← Green brand  (com.clientb.app)
        ├── app/
        └── config/

Each client app links to @org/framework via a local file: path during development and will point to the published npm package in CI/production.


Architecture

Provider Stack

Every screen in a client app is wrapped in this provider hierarchy (outermost → innermost):

<ThemeProvider>            — Design tokens (colors, typography, spacing, radius)
  <StringProvider>         — Localised/branded string resources
    <CallbackProvider>     — Injectable business-logic hooks (analytics, auth, formatting)
      <FeatureFlagProvider>— Remote flags + AsyncStorage cache
        <NavigationIndependentTree>   — Isolates nav tree from expo-router's container
          <NavigationContainer>
            <RootNavigator>           — Renders flag-active tab modules

NavigationIndependentTree is required when embedding the framework inside an expo-router shell, which already has its own NavigationContainer.

Data Flow

App boots
  → FeatureFlagProvider fetches GET /flags?appId=com.clienta.app
  → Flags cached in AsyncStorage (offline fallback)
  → RootNavigator reads flags, renders only active tab screens
  → Each tab module lazy-loaded on first visit

Quick Start

1. Install the framework

# In your client app directory
npm install @org/framework
# or, for local development (monorepo):
# package.json: "@org/framework": "file:../../framework"

2. Install peer dependencies

npm install expo react react-native \
  @react-navigation/native @react-navigation/bottom-tabs @react-navigation/stack \
  @react-native-async-storage/async-storage \
  expo-router expo-linking expo-constants expo-font expo-splash-screen expo-status-bar \
  react-native-screens react-native-safe-area-context \
  react-native-gesture-handler react-native-reanimated

3. Create your app config

// config/appConfig.ts
import type { FrameworkConfig } from '@org/framework';

export const appConfig: FrameworkConfig = {
  appId: 'com.myapp.brand',
  apiBaseUrl: 'https://api.mybrand.com',

  theme: {
    colors: {
      primary: '#FF5722',
      primaryDark: '#E64A19',
    },
  },

  strings: {
    appName: 'My Brand',
    welcomeTitle: 'Welcome to My Brand',
  },

  callbacks: {
    trackEvent: (event, properties) => {
      MyAnalytics.track(event, properties);
    },
    formatCurrency: (amount, currency) =>
      new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount),
    formatDate: (date) =>
      new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date),
  },
};

4. Mount FrameworkApp

// app/index.tsx  (expo-router entry)
import { FrameworkApp } from '@org/framework';
import { appConfig } from '../config/appConfig';

export default function App() {
  return <FrameworkApp config={appConfig} />;
}

5. Configure Metro for monorepo (local dev only)

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const config = getDefaultConfig(__dirname);
const frameworkPath = path.resolve(__dirname, '../../framework');

config.watchFolders = [frameworkPath];

// Force singleton packages to always resolve from the app's node_modules
// (prevents duplicate React / React Navigation instances from framework/node_modules)
config.resolver.resolveRequest = (context, moduleName, platform) => {
  const singletons = ['react', 'react-native', '@react-navigation/native', /* ... */];
  if (singletons.includes(moduleName)) {
    return { type: 'sourceFile', filePath: require.resolve(
      path.resolve(__dirname, 'node_modules', moduleName)
    )};
  }
  return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;

FrameworkConfig Reference

interface FrameworkConfig {
  appId:      string;                        // Required. Bundle ID / package name
  apiBaseUrl: string;                        // Required. Base URL for the flags API
  theme?:     DeepPartial<ThemeTokens>;      // Optional. Deep-merged over DEFAULT_THEME
  strings?:   Partial<StringTokens>;         // Optional. Shallow-merged over DEFAULT_STRINGS
  assets?:    Partial<AssetTokens>;          // Optional. Logo, splash, avatar images
  callbacks?: Partial<FrameworkCallbacks>;   // Optional. Analytics, auth, formatting
}

| Key | Type | Required | Description | |-----|------|----------|-------------| | appId | string | ✅ | Unique app identifier sent to the flags API | | apiBaseUrl | string | ✅ | Base URL — framework calls GET {apiBaseUrl}/flags?appId={appId} | | theme | DeepPartial<ThemeTokens> | — | Override any design token; unspecified keys fall back to defaults | | strings | Partial<StringTokens> | — | Override any string; unspecified keys fall back to English defaults | | assets | Partial<AssetTokens> | — | logo, logoLight, splashImage (use require() references) | | callbacks | Partial<FrameworkCallbacks> | — | Override analytics, auth events, currency/date formatting |


Theme Tokens

Accessed via the useTheme() hook. All tokens have defaults — override only the subset you need.

Colors (ThemeTokens.colors)

| Token | Default | Description | |-------|---------|-------------| | primary | #007AFF | Main brand color (buttons, active states) | | primaryDark | #0056CC | Dark variant (pressed states, gradients) | | primaryLight | #4DA3FF | Light variant (highlights, tinted backgrounds) | | secondary | #5856D6 | Accent color | | secondaryDark | #3634A3 | Dark accent | | secondaryLight | #8E8CF0 | Light accent | | background | #FFFFFF | Screen background | | surface | #F2F2F7 | Card / elevated surface | | error | #FF3B30 | Destructive actions, error states | | success | #34C759 | Confirmations, success states | | warning | #FF9500 | Warnings, cautions | | info | #5AC8FA | Informational | | text | #000000 | Primary text | | textSecondary | #6C6C70 | Secondary / descriptive text | | textDisabled | #AEAEB2 | Disabled / placeholder text | | border | #C6C6C8 | Input borders, dividers | | divider | #E5E5EA | List separators | | overlay | rgba(0,0,0,0.4) | Modal overlays |

Typography (ThemeTokens.typography)

| Token | Default | Description | |-------|---------|-------------| | fontFamily | System default | Regular weight font | | fontFamilyMedium | System default | Medium weight font | | fontFamilyBold | System default | Bold weight font | | fontSize.xs | 11 | Extra-small (captions) | | fontSize.sm | 13 | Small (secondary labels) | | fontSize.base | 15 | Base body text | | fontSize.md | 17 | Medium (subheadings) | | fontSize.lg | 20 | Large (section titles) | | fontSize.xl | 24 | Extra-large | | fontSize.2xl | 28 | 2× extra-large | | fontSize.3xl | 34 | Display / hero text | | fontWeight.regular | '400' | — | | fontWeight.medium | '500' | — | | fontWeight.semibold | '600' | — | | fontWeight.bold | '700' | — | | lineHeightMultiplier | 1.4 | Multiplied by fontSize for lineHeight |

Spacing (ThemeTokens.spacing)

| Token | Default (px) | |-------|-------------| | xs | 4 | | sm | 8 | | md | 16 | | lg | 24 | | xl | 32 | | 2xl | 48 |

Border Radius (ThemeTokens.radius)

| Token | Default (px) | |-------|-------------| | sm | 4 | | md | 8 | | lg | 16 | | full | 9999 |


String Tokens

Accessed via useStrings(). Override any subset in your appConfig.strings.

| Group | Key | Default | |-------|-----|---------| | App | appName | 'App' | | Onboarding | welcomeTitle | 'Welcome' | | | welcomeSubtitle | 'Get started today' | | | getStarted | 'Get Started' | | Auth | signIn | 'Sign In' | | | signUp | 'Sign Up' | | | signOut | 'Sign Out' | | | emailLabel | 'Email' | | | passwordLabel | 'Password' | | | forgotPassword | 'Forgot Password?' | | Navigation | homeTab | 'Home' | | | profileTab | 'Profile' | | | settingsTab | 'Settings' | | | paymentsTab | 'Payments' | | Common | ok | 'OK' | | | cancel | 'Cancel' | | | confirm | 'Confirm' | | | back | 'Back' | | | next | 'Next' | | | save | 'Save' | | | delete | 'Delete' | | | loading | 'Loading...' | | | error | 'Something went wrong' | | | retry | 'Try Again' | | Errors | networkError | 'No internet connection' | | | genericError | 'An unexpected error occurred' |


Callbacks Reference

Accessed via useCallbacks(). All callbacks default to no-ops — your app must override them with real implementations.

| Callback | Signature | Default | Description | |----------|-----------|---------|-------------| | onAuthSuccess | (user: User) => void | no-op | Called after successful authentication | | onAuthSignOut | () => void | no-op | Called when the user signs out | | trackEvent | (event: string, properties?: Record<string, unknown>) => void | no-op | Analytics event tracking | | formatCurrency | (amount: number, currency: string) => string | '$0.00' placeholder | Format a monetary value | | formatDate | (date: Date) => string | ISO string | Format a date for display | | onDeepLink | (url: string) => void | no-op | Called when the app handles a deep link | | onModuleActivated | (moduleName: string) => void | no-op | Called when a feature-flagged module becomes visible |

Example override:

callbacks: {
  trackEvent: (event, properties) => {
    Segment.track(event, { ...properties, appId: 'com.myapp' });
  },
  formatCurrency: (amount, currency) =>
    new Intl.NumberFormat('de-DE', { style: 'currency', currency }).format(amount),
  formatDate: (date) =>
    new Intl.DateTimeFormat('de-DE').format(date),
  onAuthSuccess: (user) => {
    Sentry.setUser({ id: user.id, email: user.email });
  },
},

Feature Flags

How It Works

On mount, FeatureFlagProvider calls:

GET {apiBaseUrl}/flags?appId={appId}

Expected response:

{
  "module_home": true,
  "module_profile": true,
  "module_settings": true,
  "module_payments": true
}

The response is stored in AsyncStorage under @framework/feature_flags. On subsequent launches, the cached flags are used immediately while a fresh fetch happens in the background. If the API is unreachable (no network, timeout after 5 s), the cache is used as a fallback. If there is no cache, the built-in defaults apply.

Default Flag Values

| Flag | Key | Default | Description | |------|-----|---------|-------------| | Home module | module_home | true | Home tab | | Profile module | module_profile | true | Profile tab | | Settings module | module_settings | true | Settings tab | | Payments module | module_payments | false | Payments tab — disabled by default, enable per app via API |

Enabling a Module Remotely

No app update required. Simply change your flags API response for a given appId:

// GET https://api.clienta.com/flags?appId=com.clienta.app
{
  "module_payments": true
}

The Payments tab will appear on next app launch (or after the background refresh completes).

Using Flags in Your Own Components

import { useFlag, useFlagContext } from '@org/framework';

// Single flag
function PayButton() {
  const paymentsEnabled = useFlag('module_payments');
  if (!paymentsEnabled) return null;
  return <Button title="Pay Now" />;
}

// Full context
function Dashboard() {
  const { flags, isLoading, refresh } = useFlagContext();
  if (isLoading) return <ActivityIndicator />;
  return <Text>Payments: {flags.module_payments ? 'ON' : 'OFF'}</Text>;
}

Module System

How Modules Work

Each module is a self-contained screen registered in src/modules/registry.ts. The RootNavigator reads the flag map at runtime and renders only the tabs whose flags are true. Modules are lazy-loaded — the JS bundle for each screen is only evaluated when the user first visits that tab.

FLAG API → FeatureFlagProvider → RootNavigator → renders active tabs
                                               ↳ lazy-loads each module on first visit

Registered Modules

| Module | Flag Key | Tab Icon | Default | |--------|----------|----------|---------| | Home | module_home | home | ✅ On | | Profile | module_profile | person | ✅ On | | Settings | module_settings | settings | ✅ On | | Payments | module_payments | credit-card | ❌ Off |

Tab order: Home → Payments → Profile → Settings (controlled by MODULE_ORDER in registry.ts).

Adding a New Module

  1. Create the screensrc/modules/<name>/index.tsx (must have a default export)
// src/modules/notifications/index.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { useTheme } from '../../theme/ThemeProvider';

export default function NotificationsScreen() {
  const theme = useTheme();
  return (
    <View style={{ flex: 1, backgroundColor: theme.colors.background }}>
      <Text style={{ color: theme.colors.text }}>Notifications</Text>
    </View>
  );
}
  1. Add the flag keysrc/flags/types.ts
export type FlagKey =
  | 'module_home'
  | 'module_profile'
  | 'module_settings'
  | 'module_payments'
  | 'module_notifications';  // ← add here

export const DEFAULT_FLAGS: Record<FlagKey, boolean> = {
  // ...existing flags...
  module_notifications: false,  // ← add here, default off
};
  1. Add the string keysrc/strings/defaults.ts
export const DEFAULT_STRINGS: StringTokens = {
  // ...
  notificationsTab: 'Notifications',
};
  1. Register the modulesrc/modules/registry.ts
export const MODULE_REGISTRY: Record<ModuleName, ModuleDescriptor> = {
  // ...existing modules...
  notifications: {
    flag: 'module_notifications',
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    load: () => Promise.resolve(require('./notifications')),
    tabLabelKey: 'notificationsTab',
    tabIconName: 'bell',
  },
};

export const MODULE_ORDER: ModuleName[] = [
  'home', 'payments', 'notifications', 'profile', 'settings'
];
  1. Publish & enable — Bump @org/framework version, update client apps, then enable via the flags API per appId.

Why require() instead of import()? Dynamic import('./notifications') loses its file-relative context when Metro resolves modules from a file:-linked monorepo package. require() is resolved at bundle-graph build time with the correct file context. Wrapping in Promise.resolve() preserves the () => Promise<{ default: ComponentType }> signature expected by React.lazy().


Provider Hooks

All hooks must be called from within a component tree that is a descendant of <FrameworkApp>.

| Hook | Returns | Description | |------|---------|-------------| | useTheme() | ThemeTokens | Full merged theme (colors, typography, spacing, radius) | | useStrings() | StringTokens | Full merged strings | | useCallbacks() | FrameworkCallbacks | All callback functions | | useFlagContext() | { flags: Record<FlagKey, boolean>, isLoading: boolean, refresh: () => Promise<void> } | Full flag map + refresh | | useFlag(key) | boolean | Single flag value |

import { useTheme, useStrings, useFlag } from '@org/framework';

function MyComponent() {
  const theme = useTheme();
  const strings = useStrings();
  const paymentsEnabled = useFlag('module_payments');

  return (
    <View style={{ backgroundColor: theme.colors.surface, padding: theme.spacing.md }}>
      <Text style={{ color: theme.colors.primary }}>{strings.appName}</Text>
      {paymentsEnabled && <PaymentsWidget />}
    </View>
  );
}

Peer Dependencies

These packages must be installed in the consuming app. They are not bundled in @org/framework.

| Package | Version | |---------|---------| | expo | >=54.0.0 | | react | >=19.1.0 | | react-native | >=0.81.0 | | @react-navigation/native | >=7.0.0 | | @react-navigation/bottom-tabs | >=7.0.0 | | @react-navigation/stack | >=7.0.0 | | @react-native-async-storage/async-storage | >=2.0.0 |


Development Setup

Prerequisites

  • Node.js 20.19+ (node --version)
  • npm 10+ (npm --version)
  • Expo Go on your test device (iOS App Store / Google Play)
  • Both devices on the same Wi-Fi network as your Mac

1. Clone and install

# Framework
cd framework
npm install --legacy-peer-deps

# Client A
cd ../apps/client-a
npm install --legacy-peer-deps

# Client B
cd ../apps/client-b
npm install --legacy-peer-deps

2. Run on device

cd apps/client-a
npx expo start --clear
  • Android — Press a or scan the QR code with your Android camera
  • iOS — Press i or scan the QR code with the iPhone Camera app → tap "Open in Expo Go"

Both platforms are served from the same Metro bundler session simultaneously.

3. Hot reload

Because the framework is a file: dependency, changes to any file in framework/src/ are reflected in both client apps immediately — no re-link or rebuild required. Metro watches framework/src/ via watchFolders in each app's metro.config.js.

Metro Monorepo Notes

The metro.config.js in each client app uses a custom resolver.resolveRequest to force singleton packages (react, react-native, @react-navigation/*) to always resolve from the app's node_modules, not the framework's. This prevents duplicate React instances and "Invalid hook call" errors.


Client App Examples

Client A — Red Brand (com.clienta.app)

export const appConfig: FrameworkConfig = {
  appId: 'com.clienta.app',
  apiBaseUrl: 'https://api.clienta.com',

  theme: {
    colors: {
      primary:      '#E63946',   // Brand red
      primaryDark:  '#C1121F',
      primaryLight: '#FF6B6B',
      secondary:    '#457B9D',
    },
  },

  strings: {
    appName:       'Client A',
    welcomeTitle:  'Welcome to Client A',
    welcomeSubtitle: 'Your trusted partner',
  },

  callbacks: {
    formatCurrency: (amount, currency) =>
      new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount),
    formatDate: (date) =>
      new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date),
  },
};

Client B — Green Brand (com.clientb.app)

export const appConfig: FrameworkConfig = {
  appId: 'com.clientb.app',
  apiBaseUrl: 'https://api.clientb.com',

  theme: {
    colors: {
      primary:      '#2D6A4F',   // Brand green
      primaryDark:  '#1B4332',
      primaryLight: '#52B788',
    },
  },

  strings: {
    appName:         'Client B',
    welcomeTitle:    'Welcome to Client B',
    welcomeSubtitle: 'Grow with us',
    paymentsTab:     'Wallet',   // Custom tab label
  },

  callbacks: {
    // GB locale formatting
    formatCurrency: (amount, currency) =>
      new Intl.NumberFormat('en-GB', { style: 'currency', currency }).format(amount),
  },
};

Both apps share the same framework version, navigation, and module logic. The only differences are colours, copy, and callback implementations.


Scripts

Run these from the framework/ directory:

| Script | Command | Description | |--------|---------|-------------| | Type check | npm run typecheck | Run tsc --noEmit against all source files | | Build | npm run build | Compile TypeScript to dist/ via react-native-builder-bob | | Lint | npm run lint | Run ESLint on src/ |


Tech Stack

| Technology | Version | Role | |------------|---------|------| | Expo SDK | 54 | Managed workflow, native module access | | React Native | 0.81.5 | Core framework | | React | 19.1.0 | UI runtime | | expo-router | 6.x | File-based routing in client apps | | React Navigation | 7.x | Bottom-tab + stack navigators | | AsyncStorage | 2.x | Feature flag offline cache | | TypeScript | 5.3 | Full type safety across framework + apps | | Metro | bundled with Expo | JS bundler with monorepo resolver config |


License

UNLICENSED — Internal use only. Not for public distribution.