Skip to content

Flagship 12

Jason Mosley edited this page May 20, 2024 · 6 revisions

Flagship Code™

Flagship Code™ is a configuration as code (CaC) toolkit focusing on native code generation for React Native, leveraging simplicity, extensibility, and type safety. The core foundation is based on React Native and TypeScript. To manage this core foundation, we created this toolkit.

CaC is the process of configuring services via machine-readable definition files. Our configuration files are typesafe JavaScript objects represented in TypeScript files.

Native code generation is everything from React Native essentials to third-party dependencies that require native code changes.

This toolkit can be broken into three models: Core, CLI, and Plugins.

Core

The core SDK contains utility functions, executors, and a template. Utility functions are foundational functions that executors and more complex functions are built upon to manipulate or generate native code.

CLI

The CLI SDK is a command line interface that listens for options that conditionally run executors.

Executors are complex functions that are executed at different native-specific lifecycles.

Plugins

Plugins are published or local native-specific scripts (e.g., iOS and/or Android) that manipulate or generate native code for a specific third-party SDK. These plugins are run generically based on a priority list captured in the package.json file.

Usage

At any time in the phase of your React Native project, you can opt-in to using Flagship Code™; opting in is as easy as creating a typesafe configuration file + required plugins and adding the iOS and Android directories to your .gitignore.

If you are already using Flagship Code™ - at any time, you can easily opt out from continuing to use Flagship Code™; opting out from Flagship Code™ is as easy as removing your configuration folder and iOS and Android directories from your .gitignore.

Integration

Integrating with Flagship Code™ can be broken down into: configuring assets, configuring environments and linking / generating plugins. While this may seem tedious, it will empower you with idempotent native code generation to effortlessly collaborate on building your application. Your team will no longer have to worry about maintaining native code directories for iOS and Android - just focus on React Native.

To get started, you will want to add the Flagship Code™ core and cli packages.

Configuring Assets

First, add the ios and android directories to your .gitignore. Create a .coderc directory in the root of your project. This is the Flagship Code™ runtime configuration directory where all configurations will be hosted i.e., CaC. Inside .coderc, create an env directory - where all your typed environments will exist. These are the only two mandatory directories.

Additional directories are recommended for separation of concerns. Inside .coderc create a signing directory - this will be where your native codesigning exists. For iOS, this would be certs and profiles. For Android, it will be your keystores.

It will be beneficial to break down your iOS codesigning assets into their respective environments e.g., enterprise, adhoc, store, etc. This way only a minimal set of profiles and certs are configured during adding of codesigning keys for CI/CD. Inside .coderc create an assets directory - this will be useful to host all plugin assets. To follow along, check out this example.

Your .coderc directory should now look like:

.coderc
├── assets
├── env
└── signing

Assets will generally be files related to plugins e.g., _.png for app icons, _.png for splash screen, and google-services.json for Firebase, etc. Typically, organizing assets by functionality is the most readable structure. To follow along, check out this example.

./assets
├── app-icon
│   ├── android-adaptive-background.png
│   ├── android-adaptive-foreground.png
│   ├── android-legacy.png
│   └── ios-universal.png
├── extensions
│   └── notifications
│       ├── notifications-Info.plist
│       ├── notifications.h
│       └── notifications.m
├── fonts
│   ├── Font-Bold.ttf
│   ├── Font-ExtraBold.ttf
│   ├── Font-ExtraLight.ttf
│   ├── Font-Light.ttf
│   ├── Font-Medium.ttf
│   ├── Font-Regular.ttf
│   └── Font-SemiBold.ttf
└── splash-screen
    ├── android
    │   └── generated
    │       └── logo.png
    └── ios
        └── generated
            └── logo.png

Env will be contain environment files e.g., development, production, adhoc, etc. These are TypeScript files based on the @brandingbrand/code-core exported type Config<T>. To follow along, check out this example.

./env
├── env.dev.ts
└── env.prod.ts

The signing folder will host all of your files related to codesigning. Android codesigning relates to your *.keystore. iOS codesigning relates to your certificates, entitlements and provisioning profiles. To follow along, check out this example.

./signing
├── AppleWWDRCA.cer
├── release.keystore
├── enterprise
│   ├── enterprise.cer
│   ├── enterprise.entitlements
│   ├── enterprise.mobileprovision
│   ├── enterprise.p12
│   └── enterprise_notification.mobileprovision
└── store
    ├── store.cer
    ├── store.entitlements
    ├── store.mobileprovision
    ├── store.p12
    └── store_notification.mobileprovision

Configuring Environments

Base

Environment configuration files are the foundation of Flagship Code™ - this is the Configuration as Code (CaC) content.

In the .coderc/env directory create an environment file e.g., env.prod.ts. Next, Import Config from @brandingbrand/code-core in your environment configuration file. This will allow you to generate a basic build configuration for iOS and Android. Plugin types can be imported and extended upon the base config e.g. Config & CodePluginAsset. With this simplistic approach you get a build configuration with plugins.

import {Config} from '@brandingbrand/code-core';
import {CodePluginAsset} from '@brandingbrand/code-plugin-asset';

const prod: Config & CodePluginAsset = {
  ios: {
    name: 'code',
    bundleId: 'com.code',
    displayName: 'Flagship Code™',
    .
    .
    .
  },
  android: {
    name: 'code',
    displayName: 'Flagship Code™',
    packageName: 'com.code',
    .
    .
    .
  },
  app: {},
  codePluginAsset: {
    code: {
      assetPath: ['assets/fonts'],
    },
  },
};

export default prod;

Advanced

The advanced approach will allow you take advantage of build and runtime configuration from a single environment configuration file. We will utiilize TypeScript's triple-dash directive to solve for a single source of truth.

Move the environment type to a common location inside the src/types directory. Inside the src/types directory create an app.d.ts file. This file is a type declaration file which we will use to define our overall app type.

In this file you will define the build and runtime configuration, where the interface App is your runtime configuration. This will provide us with type-hints when utilizing runtime configurations.

app.d.ts

declare module '@brandingbrand/code-app' {
  type ENV = import('@brandingbrand/code-core').Config<App> &
    import('@brandingbrand/code-plugin-asset').CodePluginAsset &
    import('@brandingbrand/code-plugin-app-icon').CodePluginAppIcon &
    import('@brandingbrand/code-plugin-fastlane').CodePluginFastlane &
    import('@brandingbrand/code-plugin-permissions').CodePluginPermissions &
    import('@brandingbrand/code-plugin-splash-screen').CodePluginSplashScreen &
    import('@brandingbrand/code-plugin-target-extension').CodePluginTargetExtension;

  interface App {
    foo: string;
    bar: number;
  }
}

If you are utilizing fsapp we will overwrite the any env type with our defined type via fsapp.d.ts file - To follow along, check out this example.

fsapp.d.ts

import '@brandingbrand/fsapp';

declare module '@brandingbrand/fsapp' {
  let env: import('@brandingbrand/code-core').Config<
    import('@brandingbrand/code-app').App
  >;
}

In your env file, .coderc/env/env.*.ts you can now utilize the triple-dash directive to utilize this type outside the src/ directory. To follow along, check out this example.

env.prod.ts

/// <reference path="../../src/types/app.d.ts" />

import {ENV} from '@brandingbrand/code-app';

const prod: ENV = {
  ios: {
    name: 'code',
    bundleId: 'com.code',
    displayName: 'Flagship Code™',
    .
    .
    .
  },
  android: {
    name: 'code',
    displayName: 'Flagship Code™',
    packageName: 'com.code',
    .
    .
    .
  },
  app: {
    foo: 'bar',
    bar: 123,
  },
  codePluginAsset: {
    code: {
      assetPath: ['assets/fonts'],
    },
  },
};

export default prod;

If you are using fsapp to access the env object - the environment configuration is now typesafe at runtime.

Plugins

Plugins are platform specific scripts that generate or manipulate native code with respect to a specific third-party depedency. Plugins are npm packages that are added as dev dependencies (only required at time of code init). They may or may not export an interface that will need to be added to your environment as shown in the previous section. Plugins are run by the plugins executor by auto-discovery.

Linking

Plugins can be remote or local for ultimate flexibility. Link your plugins by adding them to the package.json code.plugins array in a priority order. Plugins are run asynchronously from 0th-index to Nth-index (async / await).

package.json

  "code": {
    "plugins": [
      "@brandingbrand/code-plugin-asset",
      "@brandingbrand/code-plugin-permissions",
      "@brandingbrand/code-plugin-native-navigation",
      "@brandingbrand/code-plugin-app-icon",
      .
      .
      .
    ]
  }

Generating

Generating a plugin assumes you have familiarity with creating TypeScript / JavaScript packages. A plugin is a TypeScript package and must abide by the interface of having ios and android named exports that accept the Config type; exported from @brandingbrand/code-core. As noted by the interface below, these are async functions - all plugins are run via async / await.

index.d.ts

declare const ios: (config: Config) => Promise<void>;
declare const android: (config: Config) => Promise<void>;

The config interface can also be an intersection type of the Config and the interface of the plugin i.e. CodePluginAppIcon.

index.d.ts

declare const ios: (config: Config & CodePluginAppIcon) => Promise<void>;
declare const android: (config: Config & CodePluginAppIcon) => Promise<void>;

There are a number of ways to compile / bundle your TypeScript to JavaScript. In this monorepo, microbundle is utilized to compile and bundle TypeScript into JavaScript.

A very simple plugin that can be used as inspiration to generate your own plugin is the local plugin example - the only difference being that if you are distrbuting this package to npm you would not persist the dist directory. If you are making a local plugin, you should persist the dist directory. It's strongly recommended that each plugin have it's own set of unit tests than can be run via jest.

Additional Documentation

Find the full documentation for Flagship 12 here.

Clone this wiki locally