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
7 changes: 7 additions & 0 deletions __e2e__/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ exports[`shows up current config without unnecessary output 1`] = `
"<<REPLACED>>"
]
},
{
"name": "spm [action]",
"description": "Set up or maintain Swift Package Manager support for the iOS/macOS app. Actions: add, update, deinit, scaffold. With no action: add (or update if SPM is already set up).",
"options": [
"<<REPLACED>>"
]
},
{
"name": "log-ios",
"description": "starts iOS device syslog tail",
Expand Down
9 changes: 9 additions & 0 deletions __e2e__/unknown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ test('suggest matching command', () => {
(Did you mean init?)`,
);
});

test('explain missing @react-native/community-cli-plugin commands', () => {
writeFiles(DIR, {'package.json': '{}'});
const {exitCode, stderr} = runCLI(DIR, ['start'], {expectedFailure: true});
expect(exitCode).toBe(1);
expect(stderr).toContain(
'The "start" command is provided by @react-native/community-cli-plugin, which is not installed in this project.',
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,7 @@ Object {
}
`;

exports[`should not skip packages that have invalid configuration (to avoid breaking users): dependencies config 1`] = `
Object {
"react-native": Object {
"name": "react-native",
"platforms": Object {},
"root": "<<REPLACED>>/node_modules/react-native",
},
}
`;
exports[`should not skip packages that have invalid configuration (to avoid breaking users): dependencies config 1`] = `Object {}`;

exports[`should not skip packages that have invalid configuration (to avoid breaking users): logged warning 1`] = `"warn Package react-native contains invalid configuration: \\"dependency.invalidProperty\\" is not allowed. Please verify it's properly linked using \\"npx react-native config\\" command and contact the package maintainers about this."`;

Expand Down Expand Up @@ -138,14 +130,6 @@ Object {

exports[`should return dependencies from package.json 1`] = `
Object {
"react-native": Object {
"name": "react-native",
"platforms": Object {
"android": null,
"ios": null,
},
"root": "<<REPLACED>>/node_modules/react-native",
},
"react-native-test": Object {
"name": "react-native-test",
"platforms": Object {
Expand Down
29 changes: 28 additions & 1 deletion packages/cli-config/src/__tests__/index-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import slash from 'slash';
import {loadConfigAsync} from '..';
import loadConfig, {loadConfigAsync} from '..';
import {cleanup, writeFiles, getTempDirectory} from '../../../../jest/helpers';

let DIR = getTempDirectory('config_test');
Expand Down Expand Up @@ -100,6 +100,33 @@ test('should return dependencies from package.json', async () => {
expect(removeString(dependencies, DIR)).toMatchSnapshot();
});

test('does not link "react-native" as a dependency', async () => {
DIR = getTempDirectory('config_test_react_native_not_linked');
writeFiles(DIR, {
'node_modules/react-native/package.json': '{}',
'node_modules/react-native/React-Core-prebuilt.podspec': '',
'node_modules/react-native-test/package.json': '{}',
'node_modules/react-native-test/ReactNativeTest.podspec': '',
'package.json': `{
"dependencies": {
"react-native": "0.0.1",
"react-native-test": "0.0.1"
}
}`,
});
const platforms = {
ios: {
projectConfig: require(iosPath).projectConfig,
dependencyConfig: require(iosPath).dependencyConfig,
},
};

const asyncConfig = await loadConfigAsync({projectRoot: DIR, platforms});
const syncConfig = loadConfig({projectRoot: DIR, platforms});
expect(Object.keys(asyncConfig.dependencies)).toEqual(['react-native-test']);
expect(Object.keys(syncConfig.dependencies)).toEqual(['react-native-test']);
});

test('should read a config of a dependency and use it to load other settings', async () => {
DIR = getTempDirectory('config_test_settings');
writeFiles(DIR, {
Expand Down
89 changes: 65 additions & 24 deletions packages/cli-config/src/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,44 @@ const removeDuplicateCommands = <T extends boolean>(commands: Command<T>[]) => {
return Array.from(uniqueCommandsMap.values());
};

/**
* `react-native` provides the platforms that dependencies are linked into, so
* it is never linked as a dependency itself.
*/
const isLinkableDependency = (dependencyName: string) =>
dependencyName !== 'react-native';

/**
* Returns the built-in platforms to seed the config with, honoring the
* `selectedPlatform` filter the same way dependency platforms are filtered. The
* project's own and dependencies' platforms are layered on top of these.
*/
const getBasePlatforms = (
platforms: Config['platforms'] | undefined,
selectedPlatform: string | undefined,
): {[platform: string]: Config['platforms'][string]} => {
if (!platforms) {
return {};
}
if (selectedPlatform != null) {
return platforms[selectedPlatform]
? {[selectedPlatform]: platforms[selectedPlatform]}
: {};
}
return platforms;
};

/**
* Loads CLI configuration
*/
export default function loadConfig({
projectRoot = findProjectRoot(),
selectedPlatform,
platforms,
}: {
projectRoot?: string;
selectedPlatform?: string;
platforms?: Config['platforms'];
}): Config {
let lazyProject: ProjectConfig;
const userConfig = readConfigFromDisk(projectRoot);
Expand All @@ -110,7 +139,10 @@ export default function loadConfig({
dependencies: userConfig.dependencies,
commands: userConfig.commands,
healthChecks: userConfig.healthChecks || [],
platforms: userConfig.platforms,
platforms: {
...getBasePlatforms(platforms, selectedPlatform),
...userConfig.platforms,
},
assets: userConfig.assets,
get project() {
if (lazyProject) {
Expand Down Expand Up @@ -148,17 +180,19 @@ export default function loadConfig({
let config = readDependencyConfigFromDisk(root, dependencyName);

return assign({}, acc, {
dependencies: assign({}, acc.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
userConfig,
);
},
}),
dependencies: isLinkableDependency(dependencyName)
? assign({}, acc.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
userConfig,
);
},
})
: acc.dependencies,
commands: removeDuplicateCommands([
...config.commands,
...acc.commands,
Expand Down Expand Up @@ -188,9 +222,11 @@ export default function loadConfig({
export async function loadConfigAsync({
projectRoot = findProjectRoot(),
selectedPlatform,
platforms,
}: {
projectRoot?: string;
selectedPlatform?: string;
platforms?: Config['platforms'];
}): Promise<Config> {
let lazyProject: ProjectConfig;
const userConfig = await readConfigFromDiskAsync(projectRoot);
Expand All @@ -208,7 +244,10 @@ export async function loadConfigAsync({
dependencies: userConfig.dependencies,
commands: userConfig.commands,
healthChecks: userConfig.healthChecks || [],
platforms: userConfig.platforms,
platforms: {
...getBasePlatforms(platforms, selectedPlatform),
...userConfig.platforms,
},
assets: userConfig.assets,
get project() {
if (lazyProject) {
Expand Down Expand Up @@ -250,17 +289,19 @@ export async function loadConfigAsync({
);

return assign({}, acc, {
dependencies: assign({}, acc.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
userConfig,
);
},
}),
dependencies: isLinkableDependency(dependencyName)
? assign({}, acc.dependencies, {
get [dependencyName](): DependencyConfig {
return getDependencyConfig(
root,
dependencyName,
finalConfig,
config,
userConfig,
);
},
})
: acc.dependencies,
commands: removeDuplicateCommands([
...config.commands,
...acc.commands,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"@react-native-community/cli-clean": "20.2.0",
"@react-native-community/cli-config": "20.2.0",
"@react-native-community/cli-doctor": "20.2.0",
"@react-native-community/cli-platform-android": "20.2.0",
"@react-native-community/cli-platform-ios": "20.2.0",
"@react-native-community/cli-server-api": "20.2.0",
"@react-native-community/cli-tools": "20.2.0",
"@react-native-community/cli-types": "20.2.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import {Command, DetachedCommand} from '@react-native-community/cli-types';
import {commands as cleanCommands} from '@react-native-community/cli-clean';
import {commands as doctorCommands} from '@react-native-community/cli-doctor';
import {commands as configCommands} from '@react-native-community/cli-config';
import {commands as androidCommands} from '@react-native-community/cli-platform-android';
import {commands as iosCommands} from '@react-native-community/cli-platform-ios';
import init from './init';

export const projectCommands = [
...configCommands,
cleanCommands.clean,
doctorCommands.info,
...iosCommands,
...androidCommands,
] as Command[];

export const detachedCommands = [
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import type {
Config,
DetachedCommand,
} from '@react-native-community/cli-types';
import {
projectConfig as androidProjectConfig,
dependencyConfig as androidDependencyConfig,
} from '@react-native-community/cli-platform-android';
import {
projectConfig as iosProjectConfig,
dependencyConfig as iosDependencyConfig,
} from '@react-native-community/cli-platform-ios';
import childProcess from 'child_process';
import {Command as CommanderCommand} from 'commander';
import path from 'path';
Expand Down Expand Up @@ -149,6 +157,31 @@ const isCommandPassed = (commandName: string) => {
return process.argv.filter((arg) => arg === commandName).length > 0;
};

/**
* Commands that `@react-native/community-cli-plugin` registers, which projects
* must list in their own dependencies.
*/
const communityCliPluginCommands = ['bundle', 'codegen', 'spm', 'start'];

function exitIfCommunityCliPluginMissing(argv: string[]) {
const commandName = argv.slice(2).find((arg) => !arg.startsWith('-'));

if (
commandName == null ||
!communityCliPluginCommands.includes(commandName) ||
program.commands.some((cmd) => cmd.name() === commandName)
) {
return;
}

logger.error(
`The "${commandName}" command is provided by ${pico.bold(
'@react-native/community-cli-plugin',
)}, which is not installed in this project. Add it to your devDependencies, at the same version as react-native, then run your package manager's install.`,
);
process.exit(1);
}

async function setupAndRun(platformName?: string) {
// Commander is not available yet

Expand Down Expand Up @@ -197,6 +230,24 @@ async function setupAndRun(platformName?: string) {

config = await loadConfigAsync({
selectedPlatform,
// iOS and Android are core platforms bundled with the CLI, so they are
// always registered. The project's own and dependencies' configs can
// still override them.
//
// The cast is needed because the platform packages' `projectConfig`/
// `dependencyConfig` return `null` and take required params, whereas the
// `PlatformConfig` interface models these as `void`. These functions are
// the canonical implementations the CLI already calls via the config scan.
platforms: {
ios: {
projectConfig: iosProjectConfig,
dependencyConfig: iosDependencyConfig,
},
android: {
projectConfig: androidProjectConfig,
dependencyConfig: androidDependencyConfig,
},
} as Config['platforms'],
});

logger.enable();
Expand All @@ -211,6 +262,8 @@ async function setupAndRun(platformName?: string) {
for (const command of Object.values(commands)) {
attachCommand(command, config);
}

exitIfCommunityCliPluginMissing(process.argv);
} catch (error) {
/**
* When there is no `package.json` found, the CLI will enter `detached` mode and a subset
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
{"path": "../cli-config"},
{"path": "../cli-doctor"},
{"path": "../cli-link-assets"},
{"path": "../cli-platform-android"},
{"path": "../cli-platform-ios"},
{"path": "../cli-server-api"},
{"path": "../cli-types"},
{"path": "../cli-tools"}
Expand Down
Loading