Skip to content

Commit 85c2c5d

Browse files
authored
breaking: Bundle iOS and Android as core platforms (#2817)
1 parent 9013a48 commit 85c2c5d

9 files changed

Lines changed: 171 additions & 42 deletions

File tree

‎__e2e__/__snapshots__/config.test.ts.snap‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ exports[`shows up current config without unnecessary output 1`] = `
5656
"<<REPLACED>>"
5757
]
5858
},
59+
{
60+
"name": "spm [action]",
61+
"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).",
62+
"options": [
63+
"<<REPLACED>>"
64+
]
65+
},
5966
{
6067
"name": "log-ios",
6168
"description": "starts iOS device syslog tail",

‎__e2e__/unknown.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ test('suggest matching command', () => {
2424
(Did you mean init?)`,
2525
);
2626
});
27+
28+
test('explain missing @react-native/community-cli-plugin commands', () => {
29+
writeFiles(DIR, {'package.json': '{}'});
30+
const {exitCode, stderr} = runCLI(DIR, ['start'], {expectedFailure: true});
31+
expect(exitCode).toBe(1);
32+
expect(stderr).toContain(
33+
'The "start" command is provided by @react-native/community-cli-plugin, which is not installed in this project.',
34+
);
35+
});

‎packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap‎

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,7 @@ Object {
8585
}
8686
`;
8787

88-
exports[`should not skip packages that have invalid configuration (to avoid breaking users): dependencies config 1`] = `
89-
Object {
90-
"react-native": Object {
91-
"name": "react-native",
92-
"platforms": Object {},
93-
"root": "<<REPLACED>>/node_modules/react-native",
94-
},
95-
}
96-
`;
88+
exports[`should not skip packages that have invalid configuration (to avoid breaking users): dependencies config 1`] = `Object {}`;
9789

9890
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."`;
9991

@@ -138,14 +130,6 @@ Object {
138130

139131
exports[`should return dependencies from package.json 1`] = `
140132
Object {
141-
"react-native": Object {
142-
"name": "react-native",
143-
"platforms": Object {
144-
"android": null,
145-
"ios": null,
146-
},
147-
"root": "<<REPLACED>>/node_modules/react-native",
148-
},
149133
"react-native-test": Object {
150134
"name": "react-native-test",
151135
"platforms": Object {

‎packages/cli-config/src/__tests__/index-test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import slash from 'slash';
3-
import {loadConfigAsync} from '..';
3+
import loadConfig, {loadConfigAsync} from '..';
44
import {cleanup, writeFiles, getTempDirectory} from '../../../../jest/helpers';
55

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

103+
test('does not link "react-native" as a dependency', async () => {
104+
DIR = getTempDirectory('config_test_react_native_not_linked');
105+
writeFiles(DIR, {
106+
'node_modules/react-native/package.json': '{}',
107+
'node_modules/react-native/React-Core-prebuilt.podspec': '',
108+
'node_modules/react-native-test/package.json': '{}',
109+
'node_modules/react-native-test/ReactNativeTest.podspec': '',
110+
'package.json': `{
111+
"dependencies": {
112+
"react-native": "0.0.1",
113+
"react-native-test": "0.0.1"
114+
}
115+
}`,
116+
});
117+
const platforms = {
118+
ios: {
119+
projectConfig: require(iosPath).projectConfig,
120+
dependencyConfig: require(iosPath).dependencyConfig,
121+
},
122+
};
123+
124+
const asyncConfig = await loadConfigAsync({projectRoot: DIR, platforms});
125+
const syncConfig = loadConfig({projectRoot: DIR, platforms});
126+
expect(Object.keys(asyncConfig.dependencies)).toEqual(['react-native-test']);
127+
expect(Object.keys(syncConfig.dependencies)).toEqual(['react-native-test']);
128+
});
129+
103130
test('should read a config of a dependency and use it to load other settings', async () => {
104131
DIR = getTempDirectory('config_test_settings');
105132
writeFiles(DIR, {

‎packages/cli-config/src/loadConfig.ts‎

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,44 @@ const removeDuplicateCommands = <T extends boolean>(commands: Command<T>[]) => {
8484
return Array.from(uniqueCommandsMap.values());
8585
};
8686

87+
/**
88+
* `react-native` provides the platforms that dependencies are linked into, so
89+
* it is never linked as a dependency itself.
90+
*/
91+
const isLinkableDependency = (dependencyName: string) =>
92+
dependencyName !== 'react-native';
93+
94+
/**
95+
* Returns the built-in platforms to seed the config with, honoring the
96+
* `selectedPlatform` filter the same way dependency platforms are filtered. The
97+
* project's own and dependencies' platforms are layered on top of these.
98+
*/
99+
const getBasePlatforms = (
100+
platforms: Config['platforms'] | undefined,
101+
selectedPlatform: string | undefined,
102+
): {[platform: string]: Config['platforms'][string]} => {
103+
if (!platforms) {
104+
return {};
105+
}
106+
if (selectedPlatform != null) {
107+
return platforms[selectedPlatform]
108+
? {[selectedPlatform]: platforms[selectedPlatform]}
109+
: {};
110+
}
111+
return platforms;
112+
};
113+
87114
/**
88115
* Loads CLI configuration
89116
*/
90117
export default function loadConfig({
91118
projectRoot = findProjectRoot(),
92119
selectedPlatform,
120+
platforms,
93121
}: {
94122
projectRoot?: string;
95123
selectedPlatform?: string;
124+
platforms?: Config['platforms'];
96125
}): Config {
97126
let lazyProject: ProjectConfig;
98127
const userConfig = readConfigFromDisk(projectRoot);
@@ -110,7 +139,10 @@ export default function loadConfig({
110139
dependencies: userConfig.dependencies,
111140
commands: userConfig.commands,
112141
healthChecks: userConfig.healthChecks || [],
113-
platforms: userConfig.platforms,
142+
platforms: {
143+
...getBasePlatforms(platforms, selectedPlatform),
144+
...userConfig.platforms,
145+
},
114146
assets: userConfig.assets,
115147
get project() {
116148
if (lazyProject) {
@@ -148,17 +180,19 @@ export default function loadConfig({
148180
let config = readDependencyConfigFromDisk(root, dependencyName);
149181

150182
return assign({}, acc, {
151-
dependencies: assign({}, acc.dependencies, {
152-
get [dependencyName](): DependencyConfig {
153-
return getDependencyConfig(
154-
root,
155-
dependencyName,
156-
finalConfig,
157-
config,
158-
userConfig,
159-
);
160-
},
161-
}),
183+
dependencies: isLinkableDependency(dependencyName)
184+
? assign({}, acc.dependencies, {
185+
get [dependencyName](): DependencyConfig {
186+
return getDependencyConfig(
187+
root,
188+
dependencyName,
189+
finalConfig,
190+
config,
191+
userConfig,
192+
);
193+
},
194+
})
195+
: acc.dependencies,
162196
commands: removeDuplicateCommands([
163197
...config.commands,
164198
...acc.commands,
@@ -188,9 +222,11 @@ export default function loadConfig({
188222
export async function loadConfigAsync({
189223
projectRoot = findProjectRoot(),
190224
selectedPlatform,
225+
platforms,
191226
}: {
192227
projectRoot?: string;
193228
selectedPlatform?: string;
229+
platforms?: Config['platforms'];
194230
}): Promise<Config> {
195231
let lazyProject: ProjectConfig;
196232
const userConfig = await readConfigFromDiskAsync(projectRoot);
@@ -208,7 +244,10 @@ export async function loadConfigAsync({
208244
dependencies: userConfig.dependencies,
209245
commands: userConfig.commands,
210246
healthChecks: userConfig.healthChecks || [],
211-
platforms: userConfig.platforms,
247+
platforms: {
248+
...getBasePlatforms(platforms, selectedPlatform),
249+
...userConfig.platforms,
250+
},
212251
assets: userConfig.assets,
213252
get project() {
214253
if (lazyProject) {
@@ -250,17 +289,19 @@ export async function loadConfigAsync({
250289
);
251290

252291
return assign({}, acc, {
253-
dependencies: assign({}, acc.dependencies, {
254-
get [dependencyName](): DependencyConfig {
255-
return getDependencyConfig(
256-
root,
257-
dependencyName,
258-
finalConfig,
259-
config,
260-
userConfig,
261-
);
262-
},
263-
}),
292+
dependencies: isLinkableDependency(dependencyName)
293+
? assign({}, acc.dependencies, {
294+
get [dependencyName](): DependencyConfig {
295+
return getDependencyConfig(
296+
root,
297+
dependencyName,
298+
finalConfig,
299+
config,
300+
userConfig,
301+
);
302+
},
303+
})
304+
: acc.dependencies,
264305
commands: removeDuplicateCommands([
265306
...config.commands,
266307
...acc.commands,

‎packages/cli/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"@react-native-community/cli-clean": "20.2.0",
2828
"@react-native-community/cli-config": "20.2.0",
2929
"@react-native-community/cli-doctor": "20.2.0",
30+
"@react-native-community/cli-platform-android": "20.2.0",
31+
"@react-native-community/cli-platform-ios": "20.2.0",
3032
"@react-native-community/cli-server-api": "20.2.0",
3133
"@react-native-community/cli-tools": "20.2.0",
3234
"@react-native-community/cli-types": "20.2.0",

‎packages/cli/src/commands/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import {Command, DetachedCommand} from '@react-native-community/cli-types';
22
import {commands as cleanCommands} from '@react-native-community/cli-clean';
33
import {commands as doctorCommands} from '@react-native-community/cli-doctor';
44
import {commands as configCommands} from '@react-native-community/cli-config';
5+
import {commands as androidCommands} from '@react-native-community/cli-platform-android';
6+
import {commands as iosCommands} from '@react-native-community/cli-platform-ios';
57
import init from './init';
68

79
export const projectCommands = [
810
...configCommands,
911
cleanCommands.clean,
1012
doctorCommands.info,
13+
...iosCommands,
14+
...androidCommands,
1115
] as Command[];
1216

1317
export const detachedCommands = [

‎packages/cli/src/index.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import type {
55
Config,
66
DetachedCommand,
77
} from '@react-native-community/cli-types';
8+
import {
9+
projectConfig as androidProjectConfig,
10+
dependencyConfig as androidDependencyConfig,
11+
} from '@react-native-community/cli-platform-android';
12+
import {
13+
projectConfig as iosProjectConfig,
14+
dependencyConfig as iosDependencyConfig,
15+
} from '@react-native-community/cli-platform-ios';
816
import childProcess from 'child_process';
917
import {Command as CommanderCommand} from 'commander';
1018
import path from 'path';
@@ -149,6 +157,31 @@ const isCommandPassed = (commandName: string) => {
149157
return process.argv.filter((arg) => arg === commandName).length > 0;
150158
};
151159

160+
/**
161+
* Commands that `@react-native/community-cli-plugin` registers, which projects
162+
* must list in their own dependencies.
163+
*/
164+
const communityCliPluginCommands = ['bundle', 'codegen', 'spm', 'start'];
165+
166+
function exitIfCommunityCliPluginMissing(argv: string[]) {
167+
const commandName = argv.slice(2).find((arg) => !arg.startsWith('-'));
168+
169+
if (
170+
commandName == null ||
171+
!communityCliPluginCommands.includes(commandName) ||
172+
program.commands.some((cmd) => cmd.name() === commandName)
173+
) {
174+
return;
175+
}
176+
177+
logger.error(
178+
`The "${commandName}" command is provided by ${pico.bold(
179+
'@react-native/community-cli-plugin',
180+
)}, 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.`,
181+
);
182+
process.exit(1);
183+
}
184+
152185
async function setupAndRun(platformName?: string) {
153186
// Commander is not available yet
154187

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

198231
config = await loadConfigAsync({
199232
selectedPlatform,
233+
// iOS and Android are core platforms bundled with the CLI, so they are
234+
// always registered. The project's own and dependencies' configs can
235+
// still override them.
236+
//
237+
// The cast is needed because the platform packages' `projectConfig`/
238+
// `dependencyConfig` return `null` and take required params, whereas the
239+
// `PlatformConfig` interface models these as `void`. These functions are
240+
// the canonical implementations the CLI already calls via the config scan.
241+
platforms: {
242+
ios: {
243+
projectConfig: iosProjectConfig,
244+
dependencyConfig: iosDependencyConfig,
245+
},
246+
android: {
247+
projectConfig: androidProjectConfig,
248+
dependencyConfig: androidDependencyConfig,
249+
},
250+
} as Config['platforms'],
200251
});
201252

202253
logger.enable();
@@ -211,6 +262,8 @@ async function setupAndRun(platformName?: string) {
211262
for (const command of Object.values(commands)) {
212263
attachCommand(command, config);
213264
}
265+
266+
exitIfCommunityCliPluginMissing(process.argv);
214267
} catch (error) {
215268
/**
216269
* When there is no `package.json` found, the CLI will enter `detached` mode and a subset

‎packages/cli/tsconfig.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
{"path": "../cli-config"},
1010
{"path": "../cli-doctor"},
1111
{"path": "../cli-link-assets"},
12+
{"path": "../cli-platform-android"},
13+
{"path": "../cli-platform-ios"},
1214
{"path": "../cli-server-api"},
1315
{"path": "../cli-types"},
1416
{"path": "../cli-tools"}

0 commit comments

Comments
 (0)