Bootstrapping
// src/main.ts
import { AppRegistry, Image, Platform, processColor } from 'react-native';
import { mount } from '@ng-native/platform';
import { currentConditions, deviceTokens, watchConditions } from '@ng-native/device';
import { getFabricUIManager, registerPlatformComponents } from '@ng-native/fabric';
import { App } from './app/app.ts';
registerPlatformComponents(Platform.OS);
AppRegistry.registerRunnable('main', ({ rootTag }) => {
const app = mount(Number(rootTag), App, getFabricUIManager(), {
processColor,
conditions: currentConditions(),
tokens: deviceTokens(),
resolveAssetSource: (value) => Image.resolveAssetSource(value as never),
});
watchConditions(app.engine);
});Options
mount(rootTag, component, fabric, options?) takes an options object that is mostly there to
close gaps a browser never had:
processColorconverts a colour string or number to whatever the platform wants, the same function React Native exports asprocessColor. Without it colours reach native unconverted.resolveAssetSourceturns whatrequire('./x.png')compiles to - an asset id - into{uri, width, height, scale}. Without it a local image is blank; a remote{uri}object happens to pass through unresolved and hides the gap until someone reaches for a bundled asset.conditionsis what@mediaresolves against: viewport width and height, colour scheme, and the reduced-motion preference. Without it every media query evaluates false.tokensseeds device-level custom properties - the hairline width, mainly - below:root, so an app's own stylesheet still wins if it sets the same name.globalStylesis the one stylesheet allowed to match a node regardless of which component created it: your Tailwind output, or any app-wide reset, goes here. See Metro and Tailwind.providersare extraProvider/EnvironmentProvidersmerged into the environment injectormountcreates, the same shape you would pass tobootstrapApplication.
Return value and watching conditions
mount returns { componentRef, applicationRef, factory, engine }. engine is worth keeping: it
carries engine.stats (commit counts and timings) and is what watchConditions re-resolves media
queries against on rotation or a system theme change. Without calling watchConditions(app.engine),
dark: and any responsive layout only ever renders whatever conditions was at mount time.
watchConditions also re-measures every text when the system text size changes; without it a
text keeps the size it was first measured at, and its glyphs are clipped once they grow.
What is in the injector
mount builds an EnvironmentInjector parented to the platform injector (via Angular's
ɵcreateOrReusePlatformInjector, exactly as internalCreateApplication does), and provides:
- Zoneless change detection, unconditionally. There is no zone.js anywhere in this stack, and no option to add it back.
RendererFactory2, as theNativeRendererFactorydescribed in the renderer page.Engine, the class itself, exposed so a component can readengine.statsor reach a node for an imperative native command.HostEngine, aliased to the sameEngineinstance (useExisting: Engine). This is the interface@ng-native/componentsactually injects, so it stays renderer-agnostic;Engineunder its own token exists for the small number of components (worklet-style,worklet-scroll,native-gesture) that need Fabric's own handles for Reanimated and gesture-handler.DOCUMENT, as a stub object ({ head: undefined, body: undefined, getElementById: () => null }). It exists because Angular'sresource()reaches forTransferState, which callsdocument.getElementByIdthe moment a resource is created - without the stub every resource in an app throws before its loader runs.PLATFORM_ID, as'native'. See below.ErrorHandler, Angular's default.
Telling native from the web
PLATFORM_ID is 'native' in an app on a device. Angular's own default, when nothing provides
one, is 'unknown', which isPlatformBrowser() and isPlatformServer() both answer false for,
so a library could not tell a native app from anything else - and one that guarded its DOM work
with !isPlatformServer(id) would reach for document and fail. isPlatformNative() asks the
question the same way Angular's two do:
import { Component, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { isPlatformNative } from '@ng-native/platform';
@Component({ selector: 'app-share', template: '...' })
export class Share {
private readonly platformId = inject(PLATFORM_ID);
share(url: string): void {
if (isPlatformNative(this.platformId)) {
// The native share sheet: see /packages/device/sharing.
} else if (isPlatformBrowser(this.platformId)) {
void navigator.share?.({ url });
}
}
}On the web the answer is 'browser': mount() provides it for an app of its
own, because it really is running in a DOM, and an island inside an
Angular web app inherits whatever that app provides. To tell iOS from Android, use
nativePlatform() from @ng-native/fabric, or the ios: and android: Tailwind variants for
styling.
Development checks
In development, mount also checks that animate.enter/animate.leave will actually do anything
and logs a console error naming the fix if not, and installs the fallback reload hook Fast Refresh
cannot cover on its own. Both depend on the polyfills withAngularNative installs - see
Metro.