React Native Legal: CLI & programmatic API - unified tooling for license attributions
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

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:
Learn more about
React Native
Here's everything we published recently on this topic.
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.
