Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ project.xcworkspace
build/
packages/*/lib/
packages/*/build/
.rnlegal/

# Expo example
examples/expo-example/android
Expand Down
1 change: 1 addition & 0 deletions examples/expo-example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './web-styles.css';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet } from 'react-native';
import { MainScreen } from 'react-native-legal-common-example-ui';
Expand Down
2 changes: 2 additions & 0 deletions examples/expo-example/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '@expo/metro-runtime';
import 'react-native-legal';
import { registerRootComponent } from 'expo';

import App from './App';
Expand Down
12 changes: 9 additions & 3 deletions examples/expo-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@
"start": "expo start --dev-client",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"web": "yarn legal-generate:web && expo start --web",
"legal-generate:web": "react-native-legal legal-generate --dev-deps-mode=none --include-optional-deps --transitive-deps-mode=all",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
"expo": "^54.0.0",
"expo-build-properties": "~1.0.8",
"expo-splash-screen": "~31.0.9",
"expo-status-bar": "~3.0.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-legal": "workspace:*",
"react-native-legal-common-example-ui": "workspace:*",
"react-native-safe-area-context": "5.6.1"
"react-native-safe-area-context": "5.6.1",
"react-native-web": "^0.21.0"
},
"devDependencies": {
"@babel/core": "7.27.4",
"@babel/preset-env": "7.27.2",
"@types/react": "~19.1.12"
"@types/react": "~19.1.12",
"@types/react-dom": "^19",
"@types/react-native-web": "^0"
},
"private": true
}
3 changes: 3 additions & 0 deletions examples/expo-example/web-styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dialog.rnl--dialog {
border-color: blueviolet;
}
120 changes: 120 additions & 0 deletions packages/react-native-legal/bin/commands/legal-generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const fs = require('node:fs');
const path = require('node:path');

const { scanDependencies } = require('@callstack/licenses');

const { createPluginScanOptionsFactory } = require('../../plugin-utils/build/common');

/**
* @param {import('commander').Command} program
* @returns {import('commander').Command}
*/
function generateLegal(program) {
return program
.command('legal-generate')
.description('Set up all native boilerplate for OSS licenses notice')
.option(
'--tm, --transitive-deps-mode [mode]',
'Controls, which transitive dependencies are included:' +
`\n\u2063\t- 'all',` +
`\n\u2063\t- 'from-external-only' (only transitive dependencies of direct dependencies specified by non-workspace:... specifiers),` +
`\n\u2063\t- 'from-workspace-only' (only transitive dependencies of direct dependencies specified by \`workspace:\` specifier),` +
`\n\u2063\t- 'none'` +
'\n', // newline for auto-description of the default value
(val) => {
if (val === 'all' || val === 'from-external-only' || val === 'from-workspace-only' || val === 'none') {
return val;
}

return 'all';
},
'all',
)
.option(
'--dm, --dev-deps-mode [mode]',
'Controls, whether and how development dependencies are included:' +
`\n\u2063\t- 'root-only' (only direct devDependencies from the scanned project's root package.json)` +
`\n\u2063\t- 'none'` +
'\n', // newline for auto-description of the default value
(val) => {
if (val === 'root-only') {
return val;
}

return 'none';
},
'none',
)
.option(
'--od, --include-optional-deps [include]',
'Whether to include optionalDependencies in the scan; other flags apply',
(val) => val !== 'false',
true,
)
.action((options) => {
const repoRootPath = path.resolve(process.cwd());
const packageJsonPath = path.join(repoRootPath, 'package.json');

if (!fs.existsSync(packageJsonPath)) {
console.error(`package.json not found at ${packageJsonPath}`);
process.exit(1);
}

/** @type {import('../plugin-utils/build/types').PluginScanOptions} */
const { devDepsMode, includeOptionalDeps, transitiveDepsMode } = options;

const scanOptionsFactory = createPluginScanOptionsFactory({
devDepsMode,
includeOptionalDeps,
transitiveDepsMode,
});

const licenses = scanDependencies(packageJsonPath, scanOptionsFactory);

const payload = Object.entries(licenses)
.map(([packageKey, licenseObj]) => {
return {
name: licenseObj.name,
version: licenseObj.version,
content: licenseObj.content ?? licenseObj.type ?? 'UNKNOWN',
packageKey,
...(licenseObj.url && { source: licenseObj.url }),
};
})
.toSorted((first, second) => {
if (!first.version || !second.version) {
return first.name > second.name;
}

if (first.name !== second.name) {
return first.name > second.name ? 1 : -1;
}

const [firstMajor, firstMinor, firstPatch] = first.version.split('.').filter(Boolean);
const [secondMajor, secondMinor, secondPatch] = second.version.split('.').filter(Boolean);

return `${first.name}.${firstMajor.padStart(10, '0')}.${(firstMinor ?? '0').padStart(10, '0')}.${(
firstPatch ?? '0'
).padStart(10, '0')}` >
`${second.name}.${secondMajor.padStart(10, '0')}.${(secondMinor ?? '0').padStart(10, '0')}.${(
secondPatch ?? '0'
).padStart(10, '0')}`
? 1
: -1;
});

const rnLegalConfigPath = path.join(__dirname, '..', '..', '.rnlegal');

if (!fs.existsSync(rnLegalConfigPath)) {
fs.mkdirSync(rnLegalConfigPath);
}

const rnLegalConfigLibrariesPath = path.join(rnLegalConfigPath, 'libraries.json');

fs.writeFileSync(rnLegalConfigLibrariesPath, JSON.stringify(payload), { encoding: 'utf-8' });

console.log(`✅ Output written to ${rnLegalConfigLibrariesPath}`);
});
}

module.exports = generateLegal;
28 changes: 28 additions & 0 deletions packages/react-native-legal/bin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
const process = require('node:process');

// eslint-disable-next-line import/no-extraneous-dependencies
const { Command } = require('commander');

const { version } = require('../package.json');

const generateLegal = require('./commands/legal-generate');

const program = new Command();

program.name('react-native-legal For Web').description('Scan dependencies for Web projects').version(version);

generateLegal(program);

program
.command('help', { isDefault: false })
.description('Show help message')
.action(() => {
program.outputHelp();
});

if (!process.argv.slice(2).length) {
program.outputHelp();
} else {
program.parse(process.argv);
}
2 changes: 2 additions & 0 deletions packages/react-native-legal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"main": "lib/commonjs/index",
"module": "lib/module/index",
"types": "lib/typescript/index.d.ts",
"bin": "./bin/index.js",
"react-native": "src/index",
"source": "src/index",
"repository": {
Expand Down Expand Up @@ -59,6 +60,7 @@
},
"dependencies": {
"@callstack/licenses": "^0.3.0",
"commander": "^14.0.0",
"glob": "^7.1.3",
"xcode": "^3.0.1",
"xml2js": "^0.6.2"
Expand Down
91 changes: 91 additions & 0 deletions packages/react-native-legal/src/ReactNativeLegal.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import './ReactNativeLegalStyles.css';
import type { LibrariesResult } from './NativeReactNativeLegal';

function createElementWithClassName<K extends keyof HTMLElementTagNameMap>(tagName: K, className: string) {
const element = document.createElement(tagName);

element.className = className;

return element;
}

export const ReactNativeLegal = {
getLibrariesAsync: () => {
const payload = require(`react-native-legal/.rnlegal/libraries.json`);

return Promise.resolve<LibrariesResult>({
data: payload.map((library: any) => ({
id: library.packageKey,
name: library.packageKey,
licenses: [{ licenseContent: library.content }],
})),
});
},
launchLicenseListScreen: (licenseHeaderText?: string) => {
const payload = require(`react-native-legal/.rnlegal/libraries.json`);

const main = createElementWithClassName('main', 'rnl--main');
const closeBtn = createElementWithClassName('span', 'rnl--close-button');

closeBtn.innerHTML = '&times;';

const closeBtnContainer = createElementWithClassName('div', 'rnl--close-button-container');

closeBtnContainer.ariaLabel = 'Close licenses dialog';
closeBtnContainer.role = 'button';
closeBtnContainer.appendChild(closeBtn);

const headerContainer = createElementWithClassName('header', 'rnl--header-container');

headerContainer.appendChild(closeBtnContainer);

main.appendChild(headerContainer);

if (licenseHeaderText) {
const header = createElementWithClassName('h1', 'rnl--header');

header.innerText = licenseHeaderText;

headerContainer.appendChild(header);
}

payload.forEach((library: any) => {
const summary = createElementWithClassName('summary', 'rnl--summary');

summary.innerText = library.packageKey;

const content = createElementWithClassName('p', 'rnl--summary-content');

content.innerText = library.content;

const details = createElementWithClassName('details', 'rnl--summary-details');

details.appendChild(summary);
details.appendChild(content);

main.appendChild(details);
});

const dialog = createElementWithClassName('dialog', 'rnl--dialog');

dialog.appendChild(main);

document.querySelector('body')?.appendChild(dialog);

closeBtnContainer.addEventListener(
'click',
() => {
document.querySelector('body')?.removeChild(dialog);
},
{ once: true },
);
dialog.addEventListener(
'close',
() => {
document.querySelector('body')?.removeChild(dialog);
},
{ once: true },
);
dialog.showModal();
},
};
36 changes: 36 additions & 0 deletions packages/react-native-legal/src/ReactNativeLegalStyles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
dialog.rnl--dialog {
height: 90%;
width: 90%;
border-radius: 20px;
}

dialog.rnl--dialog::backdrop {
background-color: rgba(0,0,0,0.4);
}

main.rnl--main {
display: flex;
align-self: stretch;
flex: 1;
flex-direction: column;
overflow: scroll;
width: 100%;
}

.rnl--close-button {
font-size: 36px;
font-weight: 600;
padding: 10px;
}

header.rnl--header-container {
display: flex;
align-items: center;
flex-direction: row-reverse;
justify-content: space-between;
}

summary.rnl--summary {
font-size: 20px;
margin: 10px 0px;
}
2 changes: 1 addition & 1 deletion packages/visualizer/src/components/DependencyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function DependencyGraph({ data }: DependencyGraphProps) {
);

const graphRenderJobPendingRef = useRef<boolean>(false);
const graphRenderJobDispatchRef = useRef<NodeJS.Timeout | null>(null);
const graphRenderJobDispatchRef = useRef<ReturnType<typeof setTimeout> | null>(null);

/**
* Graph render dispatch effect; used for first committing a new state
Expand Down
Loading
Loading