Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/huge-jeans-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-native-legal': minor
---

Expose AboutLibraries and LicensePlist token options in the Expo plugin and RN CLI.
22 changes: 21 additions & 1 deletion docs/docs/docs/react-native.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ This will ensure the required native dependencies are included.
+ {
+ "devDepsMode": "root-only",
+ "includeOptionalDeps": true,
+ "transitiveDepsMode": "all"
+ "transitiveDepsMode": "all",
+ "aboutLibraries": {
+ "gitHubApiToken": "YOUR_GITHUB_TOKEN"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need two different tokens for each of the platforms?

+ },
+ "licensePlist": {
+ "githubToken": "YOUR_GITHUB_TOKEN",
+ "addVersionNumbers": true
+ }
+ }
+ ]
+ ]
Expand All @@ -75,6 +82,16 @@ interface PluginOptions {
// `'none'` (no transitive dependencies of direct dependencies)
// @default `'all'`
transitiveDepsMode?: 'all' | 'from-external-only' | 'from-workspace-only' | 'none';
aboutLibraries?: {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt about renaming those fields to androidOptions and iosOptions?

// GitHub API token used by AboutLibraries to resolve metadata.
gitHubApiToken?: string;
};
licensePlist?: {
// GitHub token passed to LicensePlist (helps avoid rate limits).
githubToken?: string;
// @default `true`
addVersionNumbers?: boolean;
};
}
```

Expand All @@ -97,6 +114,9 @@ This will extract and prepare license metadata for use in your app.
| `--tm, --transitive-deps-mode [mode]` | Controls, which transitive dependencies are included: <ul><li>`'all'`</li> <li>`'from-external-only'` (only transitive dependencies of direct dependencies specified by non-workspace:... specifiers)</li> <li>`'from-workspace-only'` (only direct dependencies of direct dependencies specified by `workspace:` specifier)</li> <li>`'none'`</li></ul> | `'all'` |
| `--dm, --dev-deps-mode [mode]` | <ul><li>`'root-only'` (only direct devDependencies from the scanned project's root package.json)</li> <li>`'none'`</li></ul> | `'none'` |
| `--od, --include-optional-deps [include]` | Whether to include optionalDependencies in the scan; other flags apply | `true` |
| `--al, --about-libraries-github-api-token [token]` | GitHub API token used by AboutLibraries to resolve metadata | — |
| `--github-token [token]` | GitHub token passed to LicensePlist (helps avoid rate limits) | — |
| `--add-version-numbers [boolean]` | Whether LicensePlist should add version numbers to entries | `true` |

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from 'node:path';

import { type Types as SharedTypes, scanDependencies, writeAboutLibrariesNPMOutput } from '@callstack/licenses';

import type { AboutLibrariesOptions } from '../../../plugin-utils/build/types';

import { addListActivity } from './addListActivity';
import { applyAndConfigureAboutLibrariesPlugin } from './applyAndConfigureAboutLibrariesPlugin';
import { declareAboutLibrariesPlugin } from './declareAboutLibrariesPlugin';
Expand All @@ -13,7 +15,11 @@ import { declareAboutLibrariesPlugin } from './declareAboutLibrariesPlugin';
* It scans the NPM dependencies, generates AboutLibraries-compatible metadata for them,
* installs & configures AboutLibraries Gradle plugin and adds Android Activity with a list of dependencies and their licenses
*/
export function androidCommand(androidProjectPath: string, scanOptionsFactory: SharedTypes.ScanPackageOptionsFactory) {
export function androidCommand(
androidProjectPath: string,
scanOptionsFactory: SharedTypes.ScanPackageOptionsFactory,
aboutLibraries?: AboutLibrariesOptions,
) {
const licenses = scanDependencies(
path.join(path.resolve(androidProjectPath, '..'), 'package.json'),
scanOptionsFactory,
Expand All @@ -27,6 +33,6 @@ export function androidCommand(androidProjectPath: string, scanOptionsFactory: S
writeAboutLibrariesNPMOutput(licenses, androidProjectPath);

declareAboutLibrariesPlugin(androidProjectPath);
applyAndConfigureAboutLibrariesPlugin(androidProjectPath);
applyAndConfigureAboutLibrariesPlugin(androidProjectPath, aboutLibraries);
addListActivity(androidProjectPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';

import { applyAndConfigureAboutLibrariesPluginUtil } from '../../../plugin-utils/build/android';
import type { AboutLibrariesOptions } from '../../../plugin-utils/build/types';

import { modifyFileContent } from './utils';

Expand All @@ -10,11 +11,13 @@ import { modifyFileContent } from './utils';
*
* NOTE: As of now, it doesn't support build.gradle.kts (Gradle Kotlin Script)
*/
export function applyAndConfigureAboutLibrariesPlugin(androidProjectPath: string) {
export function applyAndConfigureAboutLibrariesPlugin(androidProjectPath: string, options?: AboutLibrariesOptions) {
if (fs.existsSync(path.join(androidProjectPath, 'app', 'build.gradle.kts'))) {
console.warn('Gradle Kotlin scripts are not supported yet');
} else if (fs.existsSync(path.join(androidProjectPath, 'app', 'build.gradle'))) {
modifyFileContent(path.join(androidProjectPath, 'app', 'build.gradle'), applyAndConfigureAboutLibrariesPluginUtil);
modifyFileContent(path.join(androidProjectPath, 'app', 'build.gradle'), (content) =>
applyAndConfigureAboutLibrariesPluginUtil(content, options),
);
} else {
console.warn('Cannot find app/build.gradle file');
}
Expand Down
10 changes: 6 additions & 4 deletions packages/react-native-legal/bare-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { createPluginScanOptionsFactory } from '../../plugin-utils/build/common';
import type { PluginScanOptions } from '../../plugin-utils/build/types';
import type { PluginOptions, PluginScanOptions } from '../../plugin-utils/build/types';

import { androidCommand } from './android/androidCommand';
import { iosCommand } from './ios/iosCommand';

function generateLegal(
androidProjectPath: string | undefined,
iosProjectPath: string | undefined,
pluginScanOptions: PluginScanOptions,
pluginOptions: PluginOptions,
) {
const { aboutLibraries, licensePlist, ...scanOptions } = pluginOptions;
const pluginScanOptions: PluginScanOptions = scanOptions;
const scanOptionsFactory = createPluginScanOptionsFactory(pluginScanOptions);

if (androidProjectPath) {
androidCommand(androidProjectPath, scanOptionsFactory);
androidCommand(androidProjectPath, scanOptionsFactory, aboutLibraries);
}

if (iosProjectPath) {
iosCommand(iosProjectPath, scanOptionsFactory);
iosCommand(iosProjectPath, scanOptionsFactory, licensePlist);
}
}

Expand Down
10 changes: 8 additions & 2 deletions packages/react-native-legal/bare-plugin/src/ios/iosCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import path from 'node:path';

import { type Types as SharedTypes, scanDependencies, writeLicensePlistNPMOutput } from '@callstack/licenses';

import type { LicensePlistOptions } from '../../../plugin-utils/build/types';

import { addSettingsBundle } from './addSettingsBundle';
import { registerLicensePlistBuildPhase } from './registerLicensePlistBuildPhase';

Expand All @@ -11,11 +13,15 @@ import { registerLicensePlistBuildPhase } from './registerLicensePlistBuildPhase
* It scans the NPM dependencies, generates LicensePlist-compatible metadata for them,
* configures Settings.bundle and registers a shell script generating LicensePlist metadata for iOS dependencies
*/
export function iosCommand(iosProjectPath: string, scanOptionsFactory: SharedTypes.ScanPackageOptionsFactory) {
export function iosCommand(
iosProjectPath: string,
scanOptionsFactory: SharedTypes.ScanPackageOptionsFactory,
licensePlist?: LicensePlistOptions,
) {
const licenses = scanDependencies(path.join(path.resolve(iosProjectPath, '..'), 'package.json'), scanOptionsFactory);

writeLicensePlistNPMOutput(licenses, iosProjectPath);

addSettingsBundle(iosProjectPath);
registerLicensePlistBuildPhase(iosProjectPath);
registerLicensePlistBuildPhase(iosProjectPath, licensePlist);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';

import { registerLicensePlistBuildPhaseUtil } from '../../../plugin-utils/build/ios';
import type { LicensePlistOptions } from '../../../plugin-utils/build/types';

import { getAllApplicationNativeTargets, getIOSPbxProj } from './utils';

Expand All @@ -10,12 +11,12 @@ import { getAllApplicationNativeTargets, getIOSPbxProj } from './utils';
* It scans available application native targets and for each of them
* creates and links a shell script build phase responsible for iOS native deps license metadata
*/
export function registerLicensePlistBuildPhase(iosProjectPath: string) {
export function registerLicensePlistBuildPhase(iosProjectPath: string, options?: LicensePlistOptions) {
const { pbxproj, pbxprojPath } = getIOSPbxProj(iosProjectPath);
const nativeTargets = getAllApplicationNativeTargets(pbxproj);

nativeTargets.map((nativeTarget) => {
registerLicensePlistBuildPhaseUtil(nativeTarget.uuid, pbxproj);
registerLicensePlistBuildPhaseUtil(nativeTarget.uuid, pbxproj, options);
});

fs.writeFileSync(pbxprojPath, pbxproj.writeSync());
Expand Down
29 changes: 24 additions & 5 deletions packages/react-native-legal/plugin-utils/src/android.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { MiscUtils } from '@callstack/licenses';

import type { AboutLibrariesOptions } from './types';

/**
* Applies Gradle Plugin Portal & AboutLibraries Gradle plugin repositories (if needed) inside root build.gradle
*/
Expand Down Expand Up @@ -32,7 +34,10 @@ export function declareAboutLibrariesPluginUtil(androidBuildGradleContent: strin
/**
* Applies and configures AboutLibraries Grale plugin (if needed) inside app's build.gradle
*/
export function applyAndConfigureAboutLibrariesPluginUtil(androidAppBuildGradleContent: string) {
export function applyAndConfigureAboutLibrariesPluginUtil(
androidAppBuildGradleContent: string,
options?: AboutLibrariesOptions,
) {
// Apply plugin
const applyPluginBlockRegex = new RegExp(`apply\\s+plugin:\\s+['"]${PLUGIN_APPLY_BLOCK_IDENTIFIER}['"]`);

Expand All @@ -44,15 +49,29 @@ export function applyAndConfigureAboutLibrariesPluginUtil(androidAppBuildGradleC
}

// Configure plugin
const pluginConfigRegex = /aboutLibraries {/;
const pluginConfigRegex = /aboutLibraries\s*{/;
const hasConfigBlock = androidAppBuildGradleContent.match(pluginConfigRegex)?.length;
const gitHubApiToken = options?.gitHubApiToken;

if (!hasConfigBlock) {
const tokenLine = gitHubApiToken ? ` gitHubApiToken = "${gitHubApiToken}"\n` : '';

if (!androidAppBuildGradleContent.match(pluginConfigRegex)?.length) {
androidAppBuildGradleContent += '\n\naboutLibraries {\n configPath = "config"\n prettyPrint = true\n}\n';
androidAppBuildGradleContent += `\n\naboutLibraries {\n configPath = "config"\n prettyPrint = true\n${tokenLine}}\n`;
console.log('About Libraries Gradle Plugin - CONFIGURED');
} else {
console.log('About Libraries Gradle Plugin already configured - SKIP');
if (gitHubApiToken && !androidAppBuildGradleContent.match(/gitHubApiToken\s*=/)?.length) {
androidAppBuildGradleContent = androidAppBuildGradleContent.replace(
pluginConfigRegex,
`aboutLibraries {\n gitHubApiToken = "${gitHubApiToken}"`,
);
console.log('About Libraries Gradle Plugin - UPDATED');
} else {
console.log('About Libraries Gradle Plugin already configured - SKIP');
}
}

console.log('androidAppBuildGradleContent', androidAppBuildGradleContent);

return androidAppBuildGradleContent;
}

Expand Down
23 changes: 22 additions & 1 deletion packages/react-native-legal/plugin-utils/src/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from 'node:path';

import type { XcodeProject } from 'xcode';

import type { LicensePlistOptions } from './types';

/**
* Creates a Settings.bundle from a template and invokes a callback responsible for linking new file to the iOS project
*/
Expand Down Expand Up @@ -35,9 +37,28 @@ export function addSettingsBundleUtil(
/**
* Creates a shell script build phase (if needed) and links it to native targets build phases
*/
export function buildLicensePlistShellScript(options?: LicensePlistOptions) {
const args = ['${PODS_ROOT}/LicensePlist/license-plist'];

if (options?.addVersionNumbers !== false) {
args.push('--add-version-numbers');
}

if (options?.githubToken) {
const quotedToken = `'${options.githubToken.replace(/'/g, `'\\''`)}'`;

args.push(`--github-token ${quotedToken}`);
}

args.push('--output-path ./Settings.bundle');

return args.join(' ');
}

export function registerLicensePlistBuildPhaseUtil(
projectTargetId: string,
pbxproj: XcodeProject, // Xcode Pbxproj
options?: LicensePlistOptions,
) {
const nativeTargetSection = pbxproj.pbxNativeTargetSection();
const nativeTarget = nativeTargetSection[projectTargetId];
Expand All @@ -59,7 +80,7 @@ export function registerLicensePlistBuildPhaseUtil(
projectTargetId,
{
shellPath: '/bin/sh',
shellScript: '${PODS_ROOT}/LicensePlist/license-plist --add-version-numbers --output-path ./Settings.bundle',
shellScript: buildLicensePlistShellScript(options),
},
);
/**
Expand Down
14 changes: 14 additions & 0 deletions packages/react-native-legal/plugin-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,17 @@ export interface PluginScanOptions {
includeOptionalDeps: boolean;
transitiveDepsMode: 'all' | 'from-external-only' | 'from-workspace-only' | 'none';
}

export interface AboutLibrariesOptions {
gitHubApiToken?: string;
}

export interface LicensePlistOptions {
githubToken?: string;
addVersionNumbers?: boolean;
}

export interface PluginOptions extends PluginScanOptions {
aboutLibraries?: AboutLibrariesOptions;
licensePlist?: LicensePlistOptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import type { ExpoConfig } from 'expo/config';
import { withAppBuildGradle } from 'expo/config-plugins';

import { applyAndConfigureAboutLibrariesPluginUtil } from '../../../plugin-utils/build/android';
import type { AboutLibrariesOptions } from '../../../plugin-utils/build/types';

/**
* Modifies application's build.gradle with AboutLibraries plugin
*
* NOTE: As of now, it doesn't support build.gradle.kts (Gradle Kotlin Script)
*/
export function applyAndConfigureAboutLibrariesPlugin(config: ExpoConfig): ExpoConfig {
export function applyAndConfigureAboutLibrariesPlugin(config: ExpoConfig, options?: AboutLibrariesOptions): ExpoConfig {
return withAppBuildGradle(config, (exportedConfig) => {
if (exportedConfig.modResults.language === 'groovy') {
exportedConfig.modResults.contents = applyAndConfigureAboutLibrariesPluginUtil(
exportedConfig.modResults.contents,
options,
);
} else {
console.warn('Gradle Kotlin scripts are not supported yet');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { declareAboutLibrariesPlugin } from './declareAboutLibrariesPlugin';
* It scans the NPM dependencies, generates AboutLibraries-compatible metadata,
* installs & configures AboutLibraries Gradle plugin and adds Android Activity with a list of dependencies and their licenses
*/
export const withAndroidLegal: ConfigPlugin<PlatformPluginOptions> = (config, { scanOptionsFactory }) => {
export const withAndroidLegal: ConfigPlugin<PlatformPluginOptions> = (
config,
{ scanOptionsFactory, aboutLibraries },
) => {
withAndroidManifest(config, async (exportedConfig) => {
const licenses = scanDependencies(
path.join(exportedConfig.modRequest.projectRoot, 'package.json'),
Expand All @@ -26,7 +29,7 @@ export const withAndroidLegal: ConfigPlugin<PlatformPluginOptions> = (config, {
return exportedConfig;
});
config = declareAboutLibrariesPlugin(config);
config = applyAndConfigureAboutLibrariesPlugin(config);
config = applyAndConfigureAboutLibrariesPlugin(config, aboutLibraries);
config = addListActivity(config);
return config;
};
12 changes: 9 additions & 3 deletions packages/react-native-legal/plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ import type { PluginOptions } from './types';
const pak = require('react-native-legal/package.json');

const withReactNativeLegal: ConfigPlugin<PluginOptions> = (config, options) => {
const { devDepsMode = 'none', includeOptionalDeps = true, transitiveDepsMode = 'all' } = options ?? {};
const {
devDepsMode = 'none',
includeOptionalDeps = true,
transitiveDepsMode = 'all',
aboutLibraries,
licensePlist,
} = options ?? {};
const pluginScanOptions: PluginScanOptions = { devDepsMode, includeOptionalDeps, transitiveDepsMode };
const scanOptionsFactory = createPluginScanOptionsFactory(pluginScanOptions);

config = withAndroidLegal(config, { scanOptionsFactory });
config = withIosLegal(config, { scanOptionsFactory });
config = withAndroidLegal(config, { scanOptionsFactory, aboutLibraries });
config = withIosLegal(config, { scanOptionsFactory, licensePlist });

return config;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import type { ExpoConfig } from 'expo/config';
import { IOSConfig, withXcodeProject } from 'expo/config-plugins';

import { registerLicensePlistBuildPhaseUtil } from '../../../plugin-utils/build/ios';
import type { LicensePlistOptions } from '../../../plugin-utils/build/types';

/**
* Registers a shell script that invokes generation of license metadata with LicensePlist
*
* It looks for the first application native target (Expo projects will only have single native target)
* for which it creates & links a shell script build phase responsible for iOS native deps license metadata
*/
export function registerLicensePlistBuildPhase(config: ExpoConfig): ExpoConfig {
export function registerLicensePlistBuildPhase(config: ExpoConfig, options?: LicensePlistOptions): ExpoConfig {
return withXcodeProject(config, (exportedConfig) => {
const projectName = IOSConfig.XcodeUtils.getProjectName(exportedConfig.modRequest.projectRoot);
const projectTargetId = IOSConfig.XcodeUtils.getApplicationNativeTarget({
project: exportedConfig.modResults,
projectName,
}).uuid;

exportedConfig.modResults = registerLicensePlistBuildPhaseUtil(projectTargetId, exportedConfig.modResults);
exportedConfig.modResults = registerLicensePlistBuildPhaseUtil(projectTargetId, exportedConfig.modResults, options);

return exportedConfig;
});
Expand Down
Loading
Loading