React Native Legal: CLI & programmatic API - unified tooling for license attributions

Authors
Artur Morys-Magiera
Software Engineer
@
Callstack
No items found.

So far, there has been no single, unified tool to cover the common necessity touching nearly every project: giving attribution to licenses of used open source libraries. As we already mentioned in a previous blog post, most free libraries you may be using, require acknowledgement in a certain way, usually by providing the original license note with your program - which not everybody is aware of. Sometimes, developers would just ignore this problem and not give any attribution at all, or they would do this manually.

While to ease this process in the JS world there have been (only) a few tools around, they either have not maintained since years, or lacked integrations and features (export format, programmatic API, etc.). Most importantly, there was none tailored for React Native that would integrate both attributions from the native & the JS sides. Note: this article is a technical overview of the tools and provides no legal advice in any way, under no warranty ;)

If you are you building a React Native app and need a unified solution to bundle your Android & iOS native libraries' licenses attribution along with JS ones on a single screen; Expo apps are also covered. react-native-legal will collect both the JS dependencies and the native dependencies, generating a native attribution screen for both iOS and Android that can be opened imperatively from JS.

If you are instead building any JavaScript project, that case is covered as well by the CLI. license-kit - which is a Node.js CLI tool that generates a license attribution file for your project in a variety of formats (JSON, Markdown, text). No matter if it's a monorepo or a single package, license-kit will cover this case and is package-manager agnostic.

For more sophisticated use cases where more control over how the aggregated information is processed, or when the goal is to extract only selected pieces of information, the same logic has exposed via a programmatic API under the @callstack/react-native-legal-shared package.

Below, you can find examples of each of those three use cases, along with some options to tweak the behaviour.

Integration with React Native & Expo

To ease the process of integrating the license attribution screen into your React Native or Expo app, react-native-legal provides two options: for a bare metal React Native app - a CLI plugin command, and for Expo - an Expo config plugin. This means you can easily integrate it with your project. Each time the app will be built, the plugin will scan the dependencies and generate the license attribution screen automatically. You can open the generated screen using the library's Turbo Module with one method call.

We already covered this part in a previous blog post, so if you are interested, take a look at the following article.

You can read more about the RN plugins on this documentation page.

Usage in any JavaScript project

In case you have a Node.js project, a library, React / Angular website or any other thing you may be building (including RN apps), as long as you're using a common package manager - npm, pnpm, yarn or bun - you'll be able to use the license-kit CLI tool that's using that same JS package discovery & processing logic as the React Native plugin. Both monorepositories & standalone projects are supported.

To make use of it, you need to install it as a development dependency or run using npx:

npm install --save-dev license-kit
# or
npx license-kit

Then, you can generate a report of licenses or perform a copyleft license check that will try to scan if and which packages have a license requiring derivative works to be licensed under the same terms.

Licenses report

To generate a report of licenses used in your project, in the simplest form, you can run the following command:

npx license-kit report

Which by default will will scan transitive dependencies, including optional dependencies that have actually been installed, and including the development dependencies of the root package.json. The reason why root devDependencies are included by default is that usually for bundlers it does not really matter if a dependency is a development one or not, as whether it will be bundled or not depends on whether it is imported in the code.

Development dependencies filtering

There are many flags that allow for customization of the report depending on the specificity of your project. For instance, if you want to exclude the development dependencies, you can set the --dev-deps-mode flag to none:

npx license-kit report --dev-deps-mode none --output licenses.json

Format customization

By default, the report will be generated in JSON format, but you can also generate it in other formats, such as Markdown or raw text. To do that, you can use the --format flag. The below example produces a Markdown report for a repository located elsewhere than the current working directory:

npx license-kit report --format markdown --root /code/my-project --output licenses.md
screen-report-markdown

Usage in a monorepository

Another use case example: you have a monorepository containing your frontend & shared packages and want to generate a report for the frontend. Let's suppose that you want to exclude transitive dependencies coming from external packages but want to include the dependencies coming from the shared packages in your workspace that the frontend depends on (workspace: dependencies). For such a report, you can set the --transitive-deps-mode flag to from-workspace-only:

npx license-kit report --transitive-deps-mode from-workspace-only --output licenses.json

Help command

At any time, you can see the available options by running:

npx license-kit report --help

Copyleft licenses check

To check if your project involves dependencies with copyleft licenses, you can run the following command:

npx license-kit copyleft

This will print a list of errors of packages that have a strong-copyleft license, and a list of weak-copyleft packages. The source of the problem will also alter the exit code.

user@pc: $ npx license-kit copyleft
[react-native-legal] skipping @babel/compat-data@^7.22.6 could not find package.json
[react-native-legal] skipping @babel/compat-data@^7.26.5 could not find package.json
✅ No copyleft licenses found
user@pc: $ license-kit copyleft
[react-native-legal] skipping @babel/compat-data@^7.22.6 could not find package.json
[react-native-legal] skipping @babel/compat-data@^7.26.5 could not find package.json
⚠️ Weak copyleft licenses found in the following dependencies:
- mariadb: LGPL-2.1-or-later (/code/mv-project/node_modules/mariadb/LICENSE)

For this command, the same dependency filter flags as for the report command apply (--transitive-deps-mode, --dev-deps-mode, --include-optional-deps, --root) - and only such packages that are included will be checked for copyleft licenses.

Additionally, there is one boolean flag that can be used to treat weak-copyleft licenses as errors: --error-on-weak (false by default).

user@pc: $ npx license-kit copyleft --error-on-weak
[react-native-legal] skipping @babel/compat-data@^7.22.6 could not find package.json
[react-native-legal] skipping @babel/compat-data@^7.26.5 could not find package.json
❌ Weak copyleft licenses found in the following dependencies:
- mariadb: LGPL-2.1-or-later (/code/my-project/node_modules/mariadb/LICENSE)

You can read more about the CLI tool on this documentation page.

Programmatic API

In case you would want to perform a more advanced scenario, such as custom processing of the outputs, or a more sophisticated filtering of packages to be included in the scan, you can use the programmatic API that is available in the @callstack/react-native-legal-shared package. This package is a shared library that contains the logic used by both react-native-legal and license-kit, so you can use it in your own Node.js project.

Basic usage

The primary entry point is the scanDependencies function, which takes in the path to a package.json you'd like to scan and returns a list of packages with their licenses and other metadata, such as which package introduced it to the dependency tree, the resolved version, license type & text contents. You can use it as follows:

import {scanDependencies, Types} from '@callstack/react-native-legal-shared';
import * as md from 'ts-markdown-builder';

// apart from dependencies, also include devDependencies, but only from the root package.json;// also, include all transitive dependencies & optional dependenciesconst optionsFactory: Types.ScanPackageOptionsFactory = ({isRoot}) => ({
  includeDevDependencies: isRoot,
  includeTransitiveDependencies: true,
  includeOptionalDependencies: true,
});

const licenses = scanDependencies(packageJsonPath, optionsFactory);

// generate a Markdown reportconst markdownString = md
  .joinBlocks(
    Object.entries(licenses)
      .flatMap(
        ([
          packageKey,
          {
            name: packageName,
            version,
            author,
            content,
            description,
            file,
            type,
            url,
          },
        ]) => [
          md.heading(packageName, {level: 2}),
          '\n',
          `Version: ${version}<br/>\n`,
          url ? `URL: ${url}<br/>\n` : '',
          author ? `Author: ${author}<br/>\n\n` : '',
          content ?? '',
          '\n',
          description ? `Description: ${description}\n` : '',
          file ? `\nFile: ${file}\n` : '',
          type ? `Type: ${type}` : '',
          '\n',
          md.horizontalRule,
        ],
      )
      .join('\n'),
  )
  .toString();

Advanced usage

Another hypothetical example: supposing you would like to include devDependencies only from the root project, transitive dependencies from only workspace:-specified dependencies & optional dependencies only from the root or workspace:-specified dependencies, you can perform the following:

import {
  generateAboutLibrariesNPMOutput,
  generateLicensePlistNPMOutput,
  scanDependencies,
  Types,
} from '@callstack/react-native-legal-shared';

const optionsFactory: Types.ScanPackageOptionsFactory = ({
  isRoot,
  isWorkspacePackage,
}) => ({
  includeDevDependencies: isRoot,
  includeTransitiveDependencies: isWorkspacePackage,
  includeOptionalDependencies: isRoot || isWorkspacePackage,
});

const licenses = scanDependencies(packageJsonPath, optionsFactory);

The package also exposes the generateAboutLibrariesNPMOutput & generateLicensePlistNPMOutput methods used by the RN plugin to generate the AboutLibraries-compatible JSON metadata and LicensePlist-compatible metadata, respectively. You can use them as follows:

import {
  generateAboutLibrariesNPMOutput,
  generateLicensePlistNPMOutput,
  scanDependencies,
  Types,
} from '@callstack/react-native-legal-shared';
import {writeFileSync} from 'fs';
import * as md from 'ts-markdown-builder';

const optionsFactory: Types.ScanPackageOptionsFactory = ({isRoot}) => (...);

const licenses = scanDependencies(packageJsonPath, optionsFactory);

// generate AboutLibraries-compatible JSON metadataconst aboutLibrariesCompatibleReport =
  generateAboutLibrariesNPMOutput(licenses);

// generate LicensePlist-compatible metadataconst licensePlistReport = generateLicensePlistNPMOutput(licenses, iosProjectPath);

// write the reports to fileswriteFileSync('aboutLibraries.json', JSON.stringify(aboutLibrariesCompatibleReport, null, 2));
writeFileSync('licensePlist.yml', licensePlistReport);

Example: visualization of an interactive dependency graph

The default output format (JSON) carries hierarchical data about dependencies, which can be used to visualize the dependency graph of your project. For instance, we created a dependency graph of a simple project using the d3 library by utilizing d3.hierarchy & d3.tree as follows. To transform the list format to a tree-like structure for demonstration, a simplest, unoptimized approach would be as follows:

type TreeNode = {children?: TreeNode[]; meta: Types.LicenseObj; depth: number};

function buildHierarchy(_data: Types.AggregatedLicensesObj) {
  const data = _.cloneDeep(_data);

	// 1. Build the tree structure
	let root: TreeNode = {
    children: [],
    meta: {
      name: 'root',
      dependencyType: 'dependency',
      version: '0.0.0',
      requiredVersion: '0.0.0',
    },
    depth: 0,
  };

  const mapping: Record<string, TreeNode> = {};
  mapping[root.meta.name] = root;

	// 2. Recursively add children
	let depth = 0;
  while (Object.keys(data).length > 0) {
    for (const [packageKey, packageInfo] of Object.entries(data)) {
      const parentKey = packageInfo.parentPackageName
        ? `${packageInfo.parentPackageName}@${packageInfo.parentPackageResolvedVersion}`
        : 'root';

      if (parentKey in mapping) {
        mapping[parentKey].children ??= [];
        const newNode: TreeNode = {
          meta: packageInfo,
          children: [],
          depth,
        };
        mapping[parentKey].children.push(newNode);
        mapping[packageKey] = newNode;

        delete data[packageKey];// Remove processed package
      }
    }
    depth++;
  }

	// 3. Create a d3 hierarchy
	return d3.hierarchy(root, (d) => d.children);
}

Finally, we used d3.tree to create a tree layout and render it using SVG elements. The resulting graph looks as follows:

Table of contents
Need help with React or React Native projects?

We support teams building scalable apps with React and React Native.

Let’s chat
Link copied to clipboard!
//
Insights

Learn more about

React Native

Here's everything we published recently on this topic.

Sort
//
React Native

We can help you move
it forward!

At Callstack, we work with companies big and small, pushing React Native everyday.

React Native Performance Optimization

Improve React Native apps speed and efficiency through targeted performance enhancements.

Code Sharing

Implement effective code-sharing strategies across all platforms to accelerate shipping and reduce code duplication.

Mobile App Development

Launch on both Android and iOS with single codebase, keeping high-performance and platform-specific UX.

React Native Development

Hire expert React Native engineers to build, scale, or improve your app — from day one to production.