Skip to content

Commit 3fcf9d3

Browse files
feat: create plugin for Apple platforms with build:ios command (#20)
* feat: create plugin for Apple platforms * fix: properly map simulator name * fix: use logger from `@callstack/rnef-tools` * chore: replace `CLIError` usage with `Error` * fix: remove broken imports * feat: add a warning when Cocoapods are not installed * feat: add readable name inside an error message * fix: do not show prompt if value is specified with an option * fix: properly respect `--device` option with value * fix: improve error messages * feat: add `--destination` option * feat: add default build folder * fix: lint * chore: remove unsued file * chore(eslint): use base config inside apple plugin * chore: cleanup * feat: use `projectConfig` fn from `@react-native-community/cli-config-apple` * fix: remove unused vendor files responsible for creating config * fix: typescript issues * fix: replace `chalk` usage with `picocolors` * feat: add tests * change picocolors imports to fix esm error when using mjs config * use nano-spawn instead of execa * rename entrypoint * remove packager logic * add error handling around xcodebuild * rework getting mode and scheme and set defaults * fix tests --------- Co-authored-by: Michał Pierzchała <[email protected]>
1 parent 58d81e3 commit 3fcf9d3

35 files changed

+1141
-69
lines changed

packages/create-app/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"e2e": {
3131
"dependsOn": [
3232
"build",
33+
"plugin-platform-apple:build",
3334
"plugin-platform-ios:build",
3435
"plugin-platform-android:build"
3536
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# plugin-platform-ios
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Building
6+
7+
Run `nx build plugin-platform-ios` to build the library.
8+
9+
## Running unit tests
10+
11+
Run `nx test plugin-platform-ios` to execute the unit tests via [Vitest](https://vitest.dev/).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import baseConfig from '../../eslint.config.js';
2+
3+
export default baseConfig;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@callstack/rnef-plugin-platform-apple",
3+
"version": "0.0.1",
4+
"type": "module",
5+
"main": "./dist/src/index.js",
6+
"typings": "./dist/src/index.d.ts",
7+
"dependencies": {
8+
"@callstack/rnef-tools": "workspace:*",
9+
"@clack/prompts": "^0.7.0",
10+
"@react-native-community/cli-config-apple": "^15.1.2",
11+
"fast-glob": "^3.3.2",
12+
"fast-xml-parser": "^4.5.0",
13+
"nano-spawn": "^0.2.0",
14+
"picocolors": "^1.1.1",
15+
"tslib": "^2.3.0"
16+
},
17+
"devDependencies": {
18+
"@callstack/rnef-config": "workspace:*"
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "plugin-platform-apple",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "packages/plugin-platform-apple/src",
5+
"projectType": "library",
6+
"tags": [],
7+
"targets": {
8+
"build": {
9+
"executor": "@nx/js:tsc",
10+
"outputs": ["{options.outputPath}"],
11+
"options": {
12+
"outputPath": "packages/plugin-platform-apple/dist",
13+
"main": "packages/plugin-platform-apple/src/index.ts",
14+
"tsConfig": "packages/plugin-platform-apple/tsconfig.lib.json",
15+
"assets": ["packages/plugin-platform-apple/*.md"]
16+
}
17+
}
18+
}
19+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { BuilderCommand } from '../../types/index.js';
2+
import { getPlatformInfo } from '../../utils/getPlatformInfo.js';
3+
4+
export type BuildFlags = {
5+
verbose?: boolean;
6+
interactive?: boolean;
7+
mode: string;
8+
scheme: string;
9+
target?: string;
10+
extraParams?: string[];
11+
device?: string;
12+
buildFolder?: string;
13+
destination?: string;
14+
};
15+
16+
export const getBuildOptions = ({ platformName }: BuilderCommand) => {
17+
const { readableName } = getPlatformInfo(platformName);
18+
19+
return [
20+
{
21+
name: '--verbose',
22+
description: '',
23+
},
24+
{
25+
name: '-i --interactive',
26+
description:
27+
'Explicitly select which scheme and configuration to use before running a build',
28+
},
29+
{
30+
name: '--mode <string>',
31+
description:
32+
'Explicitly set the scheme configuration to use. This option is case sensitive.',
33+
},
34+
{
35+
name: '--scheme <string>',
36+
description: 'Explicitly set Xcode scheme to use',
37+
},
38+
{
39+
name: '--target <string>',
40+
description: 'Explicitly set Xcode target to use.',
41+
},
42+
{
43+
name: '--extra-params <string>',
44+
description: 'Custom params that will be passed to xcodebuild command.',
45+
parse: (val: string) => val.split(' '),
46+
},
47+
{
48+
name: '--device [string]',
49+
description:
50+
'Explicitly set the device to use by name or by unique device identifier. If the value is not provided,' +
51+
'the app will run on the first available physical device.',
52+
},
53+
{
54+
name: '--buildFolder <string>',
55+
description: `Location for ${readableName} build artifacts. Corresponds to Xcode's "-derivedDataPath".`,
56+
value: 'build',
57+
},
58+
{
59+
name: '--destination <string>',
60+
description: 'Explicitly extend destination e.g. "arch=x86_64"',
61+
},
62+
];
63+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { BuildFlags } from './buildOptions.js';
2+
import { supportedPlatforms } from '../../supportedPlatforms.js';
3+
import { ApplePlatform, XcodeProjectInfo } from '../../types/index.js';
4+
import { logger } from '@callstack/rnef-tools';
5+
import { getConfiguration } from './getConfiguration.js';
6+
import { simulatorDestinationMap } from './simulatorDestinationMap.js';
7+
import { spinner } from '@clack/prompts';
8+
import spawn from 'nano-spawn';
9+
import { selectFromInteractiveMode } from '../../utils/selectFromInteractiveMode.js';
10+
import path from 'node:path';
11+
12+
const buildProject = async (
13+
xcodeProject: XcodeProjectInfo,
14+
platformName: ApplePlatform,
15+
udid: string | undefined,
16+
args: BuildFlags
17+
) => {
18+
normalizeArgs(args, xcodeProject);
19+
const simulatorDest = simulatorDestinationMap[platformName];
20+
21+
if (!simulatorDest) {
22+
throw new Error(
23+
`Unknown platform: ${platformName}. Please, use one of: ${Object.values(
24+
supportedPlatforms
25+
).join(', ')}.`
26+
);
27+
}
28+
29+
const { scheme, mode } = args.interactive
30+
? await selectFromInteractiveMode(xcodeProject, args.scheme, args.mode)
31+
: await getConfiguration(
32+
xcodeProject,
33+
args.scheme,
34+
args.mode,
35+
platformName
36+
);
37+
38+
const xcodebuildArgs = [
39+
xcodeProject.isWorkspace ? '-workspace' : '-project',
40+
xcodeProject.name,
41+
...(args.buildFolder ? ['-derivedDataPath', args.buildFolder] : []),
42+
'-configuration',
43+
mode,
44+
'-scheme',
45+
scheme,
46+
'-destination',
47+
(() => {
48+
if (args.device && typeof args.device === 'string') {
49+
// Check if the device argument looks like a UDID (assuming UDIDs are alphanumeric and have specific length)
50+
const isUDID = /^[A-Fa-f0-9-]{25,}$/.test(args.device);
51+
if (isUDID) {
52+
return `id=${args.device}`;
53+
} else {
54+
// If it's a device name
55+
return `name=${args.device}`;
56+
}
57+
}
58+
59+
return udid
60+
? `id=${udid}`
61+
: mode === 'Debug' || args.device
62+
? `generic/platform=${simulatorDest}`
63+
: `generic/platform=${platformName}` +
64+
(args.destination ? ',' + args.destination : '');
65+
})(),
66+
];
67+
68+
if (args.extraParams) {
69+
xcodebuildArgs.push(...args.extraParams);
70+
}
71+
72+
const loader = spinner();
73+
loader.start(
74+
`Builing the app with xcodebuild for ${scheme} scheme in ${mode} mode.`
75+
);
76+
logger.debug(`Running "xcodebuild ${xcodebuildArgs.join(' ')}.`);
77+
78+
try {
79+
await spawn('xcodebuild', xcodebuildArgs, {
80+
stdio: logger.isVerbose() ? 'inherit' : ['ignore', 'ignore', 'inherit'],
81+
});
82+
loader.stop(
83+
`Built the app with xcodebuild for ${scheme} scheme in ${mode} mode.`
84+
);
85+
} catch (error) {
86+
loader.stop(
87+
'Running xcodebuild failed. Check the error message above for details.',
88+
1
89+
);
90+
throw error;
91+
}
92+
};
93+
94+
function normalizeArgs(args: BuildFlags, xcodeProject: XcodeProjectInfo) {
95+
if (!args.mode) {
96+
args.mode = 'Debug';
97+
}
98+
if (!args.scheme) {
99+
args.scheme = path.basename(
100+
xcodeProject.name,
101+
path.extname(xcodeProject.name)
102+
);
103+
}
104+
}
105+
106+
export { buildProject };
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getInfo } from '../../utils/getInfo.js';
2+
import { checkIfConfigurationExists } from '../../utils/checkIfConfigurationExists.js';
3+
import { getPlatformInfo } from './../../utils/getPlatformInfo.js';
4+
import { ApplePlatform, XcodeProjectInfo } from '../../types/index.js';
5+
import { logger } from '@callstack/rnef-tools';
6+
import color from 'picocolors';
7+
8+
export async function getConfiguration(
9+
xcodeProject: XcodeProjectInfo,
10+
inputScheme: string,
11+
inputMode: string,
12+
platformName: ApplePlatform
13+
) {
14+
const sourceDir = process.cwd();
15+
const info = await getInfo(xcodeProject, sourceDir);
16+
17+
checkIfConfigurationExists(info?.configurations ?? [], inputMode);
18+
19+
let scheme = inputScheme;
20+
21+
if (!info?.schemes?.includes(scheme)) {
22+
const { readableName } = getPlatformInfo(platformName);
23+
const fallbackScheme = `${scheme}-${readableName}`;
24+
25+
if (info?.schemes?.includes(fallbackScheme)) {
26+
logger.warn(
27+
`Scheme "${color.bold(
28+
scheme
29+
)}" doesn't exist. Using fallback scheme "${color.bold(
30+
fallbackScheme
31+
)}"`
32+
);
33+
34+
scheme = fallbackScheme;
35+
}
36+
}
37+
38+
logger.debug(
39+
`Found Xcode ${
40+
xcodeProject.isWorkspace ? 'workspace' : 'project'
41+
} "${color.bold(xcodeProject.name)}"`
42+
);
43+
44+
return { scheme, mode: inputMode };
45+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BuildFlags } from './buildOptions.js';
2+
import { buildProject } from './buildProject.js';
3+
import { BuilderCommand, ProjectConfig } from '../../types/index.js';
4+
import { logger } from '@callstack/rnef-tools';
5+
import { outro, cancel } from '@clack/prompts';
6+
7+
export const createBuild = async (
8+
platformName: BuilderCommand['platformName'],
9+
projectConfig: ProjectConfig,
10+
buildFlags: BuildFlags
11+
) => {
12+
// TODO: add logic for installing Cocoapods based on @expo/fingerprint & pod-install package.
13+
14+
const { xcodeProject, sourceDir } = projectConfig;
15+
16+
if (!xcodeProject) {
17+
logger.error(
18+
`Could not find Xcode project files in "${sourceDir}" folder. Please make sure that you have installed Cocoapods and "${sourceDir}" is a valid path`
19+
);
20+
process.exit(1);
21+
}
22+
23+
process.chdir(sourceDir);
24+
25+
try {
26+
await buildProject(xcodeProject, platformName, undefined, buildFlags);
27+
outro('Success 🎉.');
28+
} catch {
29+
cancel('Command failed.');
30+
}
31+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ApplePlatform } from '../../types/index.js';
2+
3+
export const simulatorDestinationMap: Record<ApplePlatform, string> = {
4+
ios: 'iOS Simulator',
5+
// macos: 'macOS',
6+
// visionos: 'visionOS Simulator',
7+
// tvos: 'tvOS Simulator',
8+
};

0 commit comments

Comments
 (0)