Skip to content
Merged
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/nasty-socks-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/react-native-legal-shared': minor
---

Feature: dependency scanning configuration for transitive & development dependencies
21 changes: 1 addition & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,26 +179,7 @@ For a list of supported flags and the default values, run `npx license-kit --hel

### I want to customize the presentation of the licenses in my JS/TS project

You can use the `@callstack/react-native-legal-shared` package to access the core functionalities of the license management tool. Here's a basic example of how to use it:

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

// scan dependencies of a package
const licenses = scanDependencies(packageJsonPath);

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

// generate LicensePlist-compatible metadata
const licensePlistReport = generateLicensePlistNPMOutput(licenses);
```

For more advanced usage, read the [programmatic usage documentation](https://callstackincubator.github.io/react-native-legal/docs/programmatic-usage#usage).
You can use the `@callstack/react-native-legal-shared` package to access the core functionalities of the license management tool. To do so, please read the [programmatic usage documentation](https://callstackincubator.github.io/react-native-legal/docs/programmatic-usage#usage).

## Expo

Expand Down
18 changes: 17 additions & 1 deletion docs/docs/docs/programmatic-usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ import {
generateAboutLibrariesNPMOutput,
generateLicensePlistNPMOutput,
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
const optionsFactory: Types.ScanPackageOptionsFactory = ({ isRoot }) => ({
includeDevDependencies: isRoot,
includeTransitiveDependencies: true,
});

// scan dependencies of a package
const licenses = scanDependencies(packageJsonPath);
const licenses = scanDependencies(packageJsonPath, optionsFactory);

// generate AboutLibraries-compatible JSON metadata
const aboutLibrariesCompatibleReport = generateAboutLibrariesNPMOutput(licenses);
Expand Down Expand Up @@ -58,6 +66,14 @@ const markdownString = md
.toString();
```

### Options

As you can see in the example above, filtering for the packages to be included in the scan for dependencies is based on the passed in `ScanPackageOptionsFactory`. This factory would be invoked for every scanned `package.json` file, including the root and any considered dependencies and - given a `ScanPackageOptionsFactoryPackageInfo` object informing of the context of the package being scanned - should produce an accompanying configuration that would filter this package's dependencies.

This approach gives a programmatic control at an arbitrary level of precision over filtering of dependencies to be included.

As a reference, you can see how this is used in the `license-kit` CLI in [`createScanOptionsFactory` of `scanOptionsUtils.ts`](https://github.com/callstackincubator/react-native-legal/blob/main/packages/license-kit/src/scanOptionsUtils.ts).

### API Documentation

The API documentation is published under: https://callstackincubator.github.io/react-native-legal/api/.
Expand Down
12 changes: 10 additions & 2 deletions examples/bare-example/e2e/checkLicenses/android.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
appId: com.reactnativelegalbareexample
name: "[Android] Check React Native entry in OSS libraries list"
name: '[Android] Check React Native entry in OSS libraries list'
tags:
- pull-request
- android
Expand All @@ -10,12 +10,20 @@ tags:
- startRecording: 'e2e_results/checkLicenses'
- tapOn: 'Tap to see list of OSS libraries'
- assertVisible: 'OSS Notice'
# workaround for long list issue taking ages to scroll to 'r*' packages (causing a timeout)
- repeat:
times: 7
commands:
- swipe:
start: 50%, 90%
end: 50%, 0%
duration: 50
- scrollUntilVisible:
element:
containsChild: 'react-native'
index: 0
direction: 'DOWN'
timeout: 120000
timeout: 190000
centerElement: true
speed: 70
- takeScreenshot: 'e2e_results/react-native_list_element'
Expand Down
23 changes: 15 additions & 8 deletions examples/bare-example/e2e/checkLicenses/ios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@ tags:
- startRecording: 'e2e_results/checkLicenses'
- tapOn: 'Tap to see list of OSS libraries'
- assertVisible: 'OSS Notice'
# workaround for long list issue taking ages to scroll to 'r*' packages (causing a timeout)
- repeat:
times: 2
commands:
- swipe:
start: 50%, 90%
end: 50%, 0%
duration: 50
- scrollUntilVisible:
label: Scroll to React Native library
element: "react-native"
element: 'react-native'
direction: 'DOWN'
timeout: 60000
timeout: 100000
speed: 80
- takeScreenshot: 'e2e_results/react-native_list_element'
- tapOn: "react-native"
- tapOn: 'react-native'
- takeScreenshot: 'e2e_results/react-native_entry'
- assertVisible: "react-native"
- assertVisible: "MIT License"
- assertVisible: "OSS Notice"
- tapOn: "OSS Notice"
- assertVisible: 'react-native'
- assertVisible: 'MIT License'
- assertVisible: 'OSS Notice'
- tapOn: 'OSS Notice'
- takeScreenshot: 'e2e_results/react-native_back_to_list'
- stopRecording
- killApp

3 changes: 2 additions & 1 deletion packages/license-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"clean": "rimraf build",
"build-library": "tsc -p tsconfig.json",
"dev": "npx tsx src/index.ts"
"dev": "tsx src/index.ts"
},
"keywords": [
"nodejs",
Expand All @@ -55,6 +55,7 @@
"@types/jest": "^29.5.5",
"jest": "^29.7.0",
"rimraf": "^6.0.1",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
},
"dependencies": {
Expand Down
10 changes: 9 additions & 1 deletion packages/shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import {
generateAboutLibrariesNPMOutput,
generateLicensePlistNPMOutput,
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
const optionsFactory: Types.ScanPackageOptionsFactory = ({ isRoot }) => ({
includeDevDependencies: isRoot,
includeTransitiveDependencies: true,
});

// scan dependencies of a package
const licenses = scanDependencies(packageJsonPath);
const licenses = scanDependencies(packageJsonPath, optionsFactory);

// generate AboutLibraries-compatible JSON metadata
const aboutLibrariesCompatibleReport = generateAboutLibrariesNPMOutput(licenses);
Expand Down
58 changes: 40 additions & 18 deletions packages/shared/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import path from 'path';

import glob from 'glob';

import type {
AboutLibrariesLibraryJsonPayload,
AboutLibrariesLicenseJsonPayload,
AboutLibrariesLikePackageInfo,
AggregatedLicensesObj,
LicensePlistPayload,
import {
type AboutLibrariesLibraryJsonPayload,
type AboutLibrariesLicenseJsonPayload,
type AboutLibrariesLikePackageInfo,
type AggregatedLicensesObj,
type LicensePlistPayload,
type ScanPackageOptionsFactory,
} from './types';
import { PackageUtils } from './utils';

Expand All @@ -20,6 +21,7 @@ function scanPackage(
version: string,
processedPackages: Set<string>,
result: AggregatedLicensesObj,
scanOptionsFactory: ScanPackageOptionsFactory = PackageUtils.legacyDefaultScanPackageOptionsFactory,
) {
const packageKey = `${packageName}@${version}`;

Expand Down Expand Up @@ -61,16 +63,27 @@ function scanPackage(
}

const dependencies = localPackageJson.dependencies;

const devDependencies = localPackageJson.devDependencies;
const isWorkspacePackage = version.startsWith('workspace:');

if (!isWorkspacePackage) return;
const scanOptions = scanOptionsFactory({
isRoot: false,
isWorkspacePackage,
});

if (dependencies) {
Object.entries(dependencies).forEach(([depName, depVersion]) => {
scanPackage(depName, depVersion as string, processedPackages, result);
});
// check if transitive dependencies should be scanned
if (!scanOptions.includeTransitiveDependencies) {
return;
}

[
// transitive dependencies
...(dependencies ? Object.entries(dependencies) : []),
// transitive devDependencies
...(devDependencies && scanOptions.includeDevDependencies ? Object.entries(devDependencies) : []),
].forEach(([depName, depVersion]) => {
scanPackage(depName, depVersion as string, processedPackages, result, scanOptionsFactory);
});
} catch (error) {
console.warn(`[react-native-legal] could not process package.json for ${packageName}`);
}
Expand All @@ -79,17 +92,26 @@ function scanPackage(
/**
* Scans `package.json` and searches for all packages under `dependencies` field. Supports monorepo projects.
*/
export function scanDependencies(appPackageJsonPath: string) {
export function scanDependencies(
appPackageJsonPath: string,
scanOptionsFactory: ScanPackageOptionsFactory = PackageUtils.legacyDefaultScanPackageOptionsFactory,
): AggregatedLicensesObj {
const appPackageJson = require(path.resolve(appPackageJsonPath));
const dependencies: Record<string, string> = appPackageJson.dependencies;
const devDependencies: Record<string, string> = appPackageJson.devDependencies;
const result: AggregatedLicensesObj = {};
const processedPackages = new Set<string>();

if (dependencies) {
Object.entries(dependencies).forEach(([packageName, version]) => {
scanPackage(packageName, version, processedPackages, result);
});
}
const rootScanOptions = scanOptionsFactory({ isRoot: true, isWorkspacePackage: false });

[
// dependencies
...(dependencies ? Object.entries(dependencies) : []),
// devDependencies
...(devDependencies && rootScanOptions.includeDevDependencies ? Object.entries(devDependencies) : []),
].forEach(([packageName, version]) => {
scanPackage(packageName, version, processedPackages, result, scanOptionsFactory);
});

return result;
}
Expand Down
26 changes: 26 additions & 0 deletions packages/shared/src/types/ScanPackageOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Scan options for controlling which dependencies of a package are scanned
*/
export type ScanPackageOptions = {
/** Whether transitive dependencies should be scanned */
includeTransitiveDependencies: boolean;

/** Whether to include devDependencies in the scan; include*Transitive* options apply */
includeDevDependencies: boolean;
};

/**
* Information about a single package to be scanned
*/
export type ScanPackageOptionsFactoryPackageInfo = {
/** `true` if the analyzed package.json is the root of the scanned project */
isRoot: boolean;

/** `true` if the analyzed package.json is a dependency related to the project via a `workspace:...` dependency specifier */
isWorkspacePackage: boolean;
};

/**
* Factory to create a filter for scan options for dependencies of a given package
*/
export type ScanPackageOptionsFactory = (packageInfo: ScanPackageOptionsFactoryPackageInfo) => ScanPackageOptions;
1 change: 1 addition & 0 deletions packages/shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './AboutLibrariesLibraryJsonPayload';
export * from './AboutLibrariesLicenseJsonPayload';
export * from './LicensePlistPayload';
export * from './AboutLibrariesLikePackageInfo';
export * from './ScanPackageOptions';
11 changes: 10 additions & 1 deletion packages/shared/src/utils/packageUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';

import type { LicenseObj } from '../types';
import type { LicenseObj, ScanPackageOptionsFactory } from '../types';

import { sha512 } from './miscUtils';
import { normalizeRepositoryUrl } from './repositoryUtils';
Expand Down Expand Up @@ -80,3 +80,12 @@ export function parseRepositoryFieldToUrl(json: { repository: string | { url?: s
return normalizeRepositoryUrl(json.repository);
}
}

/**
* Default value consistent with legacy behaviour assumptions for the scan package options factory
* used so as not to introduce breaking API changes to the shared package
*/
export const legacyDefaultScanPackageOptionsFactory: ScanPackageOptionsFactory = () => ({
includeTransitiveDependencies: true,
includeDevDependencies: false,
});
Loading
Loading