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 docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The following type describes the configuration of a dependency that can be set u

```ts
type DependencyConfig = {
autolinkTransitiveDependencies?: boolean;
platforms: {
android?: AndroidDependencyParams;
ios?: IOSDependencyParams;
Expand All @@ -37,6 +38,10 @@ type DependencyConfig = {

A map of specific settings that can be set per platform. The exact shape is always defined by the package that provides given platform.

### autolinkTransitiveDependencies

When set to `true`, the CLI will inspect the dependency's `peerDependencies` and attempt to autolink any peers that are also React Native native modules. The CLI does not install those peers for the user, but they will be linked automatically whenever they are present in `node_modules`. Use this if your library relies on a native peer dependency (for example, [`react-native-nitro-text`](https://github.com/patrickkabwe/react-native-nitro-text) depending on [`react-native-nitro-modules`](https://github.com/mrousavy/nitro)) and would otherwise require users to manually add that peer.

In most cases, as a library author, you should not need to define any of these.

The following settings are available on iOS and Android:
Expand Down
48 changes: 48 additions & 0 deletions packages/cli-config/src/__tests__/index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,54 @@ module.exports = {
`);
});

test('autolinks transitive peer dependencies when enabled by a library', async () => {
DIR = getTempDirectory('config_test_transitive_peers');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
'package.json': `{
"dependencies": {
"react-native": "0.0.1",
"react-native-nitro-text": "0.0.1"
}
}`,
'node_modules/react-native-nitro-text/package.json': `{
"name": "react-native-nitro-text",
"peerDependencies": {
"react-native-nitro-modules": "1.0.0"
}
}`,
'node_modules/react-native-nitro-text/react-native.config.js': `module.exports = {
dependency: {
autolinkTransitiveDependencies: true,
},
};`,
'node_modules/react-native-nitro-modules/package.json': `{
"name": "react-native-nitro-modules",
"version": "1.0.0"
}`,
'node_modules/react-native-nitro-modules/ReactNativeNitroModules.podspec':
'',
'node_modules/react-native-nitro-modules/react-native.config.js': `module.exports = {
dependency: {
platforms: {
ios: {
podspecPath: "./ReactNativeNitroModules.podspec",
},
},
},
};`,
});

const config = await loadConfigAsync({projectRoot: DIR});
expect(Object.keys(config.dependencies)).toEqual(
expect.arrayContaining([
'react-native',
'react-native-nitro-text',
'react-native-nitro-modules',
]),
);
});

test('should apply build types from dependency config', async () => {
DIR = getTempDirectory('config_test_apply_dependency_config');
writeFiles(DIR, {
Expand Down
217 changes: 176 additions & 41 deletions packages/cli-config/src/loadConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'fs';
import {promises as fsPromises} from 'fs';
import path from 'path';
import {
UserDependencyConfig,
Expand Down Expand Up @@ -31,10 +33,15 @@ function getDependencyConfig(
config: UserDependencyConfig,
userConfig: UserConfig,
): DependencyConfig {
const {autolinkTransitiveDependencies} = config.dependency;

return merge(
{
root,
name: dependencyName,
...(autolinkTransitiveDependencies !== undefined
? {autolinkTransitiveDependencies}
: {}),
platforms: Object.keys(finalConfig.platforms).reduce(
(dependency, platform) => {
const platformConfig = finalConfig.platforms[platform];
Expand Down Expand Up @@ -84,6 +91,62 @@ const removeDuplicateCommands = <T extends boolean>(commands: Command<T>[]) => {
return Array.from(uniqueCommandsMap.values());
};

const getUserAutolinkOverride = (
dependencyName: string,
userConfig: UserConfig,
) => {
const userDependencyConfig = userConfig.dependencies[dependencyName];
if (
userDependencyConfig &&
typeof userDependencyConfig === 'object' &&
Object.prototype.hasOwnProperty.call(
userDependencyConfig,
'autolinkTransitiveDependencies',
)
) {
const value = userDependencyConfig.autolinkTransitiveDependencies;
if (typeof value === 'boolean') {
return value;
}
}
return undefined;
};

const shouldAutolinkTransitiveDependencies = (
dependencyName: string,
dependencyConfig: UserDependencyConfig,
userConfig: UserConfig,
) => {
const override = getUserAutolinkOverride(dependencyName, userConfig);
if (typeof override === 'boolean') {
return override;
}

return dependencyConfig.dependency.autolinkTransitiveDependencies === true;
};

const getPeerDependenciesSync = (dependencyRoot: string) => {
try {
const packageJsonPath = path.join(dependencyRoot, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return Object.keys(packageJson.peerDependencies || {});
} catch {
return [];
}
};

const getPeerDependenciesAsync = async (dependencyRoot: string) => {
try {
const packageJsonPath = path.join(dependencyRoot, 'package.json');
const packageJson = JSON.parse(
await fsPromises.readFile(packageJsonPath, 'utf8'),
);
return Object.keys(packageJson.peerDependencies || {});
} catch {
return [];
}
};

/**
* Loads CLI configuration
*/
Expand Down Expand Up @@ -132,51 +195,89 @@ export default function loadConfig({
},
};

const finalConfig = Array.from(
new Set([
...Object.keys(userConfig.dependencies),
...findDependencies(projectRoot),
]),
).reduce((acc: Config, dependencyName) => {
const queuedDependencies = new Set([
...Object.keys(userConfig.dependencies),
...findDependencies(projectRoot),
]);
const queue = Array.from(queuedDependencies);
const processedDependencies = new Set<string>();

let finalConfig: Config = initialConfig;

while (queue.length > 0) {
const dependencyName = queue.shift() as string;

if (processedDependencies.has(dependencyName)) {
continue;
}

const currentConfig = finalConfig;

processedDependencies.add(dependencyName);

const localDependencyRoot =
userConfig.dependencies[dependencyName] &&
userConfig.dependencies[dependencyName].root;
try {
let root =
const root =
localDependencyRoot ||
resolveNodeModuleDir(projectRoot, dependencyName);
let config = readDependencyConfigFromDisk(root, dependencyName);
const dependencyConfig = readDependencyConfigFromDisk(
root,
dependencyName,
);

return assign({}, acc, {
dependencies: assign({}, acc.dependencies, {
const nextConfig = assign({}, currentConfig, {
dependencies: assign({}, currentConfig.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
dependencyConfig,
userConfig,
);
},
}),
commands: removeDuplicateCommands([
...config.commands,
...acc.commands,
...dependencyConfig.commands,
...currentConfig.commands,
]),
platforms: {
...acc.platforms,
...(selectedPlatform && config.platforms[selectedPlatform]
? {[selectedPlatform]: config.platforms[selectedPlatform]}
...currentConfig.platforms,
...(selectedPlatform && dependencyConfig.platforms[selectedPlatform]
? {[selectedPlatform]: dependencyConfig.platforms[selectedPlatform]}
: !selectedPlatform
? config.platforms
? dependencyConfig.platforms
: undefined),
},
healthChecks: [...acc.healthChecks, ...config.healthChecks],
healthChecks: [
...currentConfig.healthChecks,
...dependencyConfig.healthChecks,
],
}) as Config;

finalConfig = nextConfig;

if (
shouldAutolinkTransitiveDependencies(
dependencyName,
dependencyConfig,
userConfig,
)
) {
const peerDependencies = getPeerDependenciesSync(root);
for (const peerDependency of peerDependencies) {
if (!queuedDependencies.has(peerDependency)) {
queuedDependencies.add(peerDependency);
queue.push(peerDependency);
}
}
}
} catch {
return acc;
continue;
}
}, initialConfig);
}

return finalConfig;
}
Expand Down Expand Up @@ -230,55 +331,89 @@ export async function loadConfigAsync({
},
};

const finalConfig = await Array.from(
new Set([
...Object.keys(userConfig.dependencies),
...findDependencies(projectRoot),
]),
).reduce(async (accPromise: Promise<Config>, dependencyName) => {
const acc = await accPromise;
const queuedDependencies = new Set([
...Object.keys(userConfig.dependencies),
...findDependencies(projectRoot),
]);
const queue = Array.from(queuedDependencies);
const processedDependencies = new Set<string>();

let finalConfig: Config = initialConfig;

while (queue.length > 0) {
const dependencyName = queue.shift() as string;

if (processedDependencies.has(dependencyName)) {
continue;
}

const currentConfig = finalConfig;

processedDependencies.add(dependencyName);

const localDependencyRoot =
userConfig.dependencies[dependencyName] &&
userConfig.dependencies[dependencyName].root;
try {
let root =
const root =
localDependencyRoot ||
resolveNodeModuleDir(projectRoot, dependencyName);
let config = await readDependencyConfigFromDiskAsync(
const dependencyConfig = await readDependencyConfigFromDiskAsync(
root,
dependencyName,
);

return assign({}, acc, {
dependencies: assign({}, acc.dependencies, {
const nextConfig = assign({}, currentConfig, {
dependencies: assign({}, currentConfig.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
dependencyConfig,
userConfig,
);
},
}),
commands: removeDuplicateCommands([
...config.commands,
...acc.commands,
...dependencyConfig.commands,
...currentConfig.commands,
]),
platforms: {
...acc.platforms,
...(selectedPlatform && config.platforms[selectedPlatform]
? {[selectedPlatform]: config.platforms[selectedPlatform]}
...currentConfig.platforms,
...(selectedPlatform && dependencyConfig.platforms[selectedPlatform]
? {[selectedPlatform]: dependencyConfig.platforms[selectedPlatform]}
: !selectedPlatform
? config.platforms
? dependencyConfig.platforms
: undefined),
},
healthChecks: [...acc.healthChecks, ...config.healthChecks],
healthChecks: [
...currentConfig.healthChecks,
...dependencyConfig.healthChecks,
],
}) as Config;

finalConfig = nextConfig;

if (
shouldAutolinkTransitiveDependencies(
dependencyName,
dependencyConfig,
userConfig,
)
) {
const peerDependencies = await getPeerDependenciesAsync(root);
for (const peerDependency of peerDependencies) {
if (!queuedDependencies.has(peerDependency)) {
queuedDependencies.add(peerDependency);
queue.push(peerDependency);
}
}
}
} catch {
return acc;
continue;
}
}, Promise.resolve(initialConfig));
}

return finalConfig;
}
2 changes: 2 additions & 0 deletions packages/cli-config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const dependencyConfig = t
.object({
dependency: t
.object({
autolinkTransitiveDependencies: t.boolean(),
platforms: map(t.string(), t.any())
.keys({
ios: t
Expand Down Expand Up @@ -120,6 +121,7 @@ export const projectConfig = t
t
.object({
root: t.string(),
autolinkTransitiveDependencies: t.boolean(),
platforms: map(t.string(), t.any()).keys({
ios: t
// IOSDependencyConfig
Expand Down
Loading