Skip to content

Commit

Permalink
CLI supports ordering of tasks
Browse files Browse the repository at this point in the history
Summary:
This gives Frameworks more control in selecting specific tasks and integrating the return types data in their UI.  For example piping `stdout` to the user or using packages like [Listr2](https://www.npmjs.com/package/listr2) to run tasks in parallel and show progress.

The ordering is suggestive (but also enforced by some assertions).  Frameworks are free to do what they want.

The order was implicit in the previous data structure with lists of Tasks, but made it difficult to tap into each async task.

I've also had to rework how we transpile the code if directly executed from the monorepo.  This keeps our:
- flow types valid,
- allows the core-cli-utils package to be built (to generate TypeScript types and a valid npm module), and
- allows direct transpiled execution as a yarn script.

Changelog: [Internal]

Reviewed By: cipolleschi

Differential Revision: D56242487
  • Loading branch information
blakef authored and facebook-github-bot committed Apr 28, 2024
1 parent 08a6774 commit cf15e6b
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type StartCommandArgs = {
https?: boolean,
maxWorkers?: number,
key?: string,
platforms?: string[],
platforms: string[],
port?: number,
resetCache?: boolean,
sourceExts?: string[],
Expand Down
10 changes: 5 additions & 5 deletions packages/core-cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
"name": "@react-native/core-cli-utils",
"version": "0.75.0-main",
"description": "React Native CLI library for Frameworks to build on",
"main": "./src/index.js",
"main": "./src/index.flow.js",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react-native.git",
"directory": "packages/core-cli-utils"
},
"exports": {
".": "./src/index.js",
".": "./src/index.flow.js",
"./package.json": "./package.json",
"./version.js": "./src/public/version.js"
},
"types": "./dist/index.d.ts",
"types": "./dist/index.flow.d.ts",
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
"keywords": [
"cli-utils",
Expand All @@ -25,8 +25,8 @@
"node": ">=18"
},
"files": [
"dist"
"src"
],
"dependencies": {},
"devDependencies": {}
}
}
34 changes: 0 additions & 34 deletions packages/core-cli-utils/src/index.js

This file was deleted.

23 changes: 23 additions & 0 deletions packages/core-cli-utils/src/monorepo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

// Should only used when called in the monorepo when we don't want to use the `yarn run build`
// step to transpile to project. When used as a vanilla npm package, it should be built and
// exported with `dist/index.flow.js` as main.
//
// The reason for this workaround is that flow-api-translator can't understand ESM and CJS style
// exports in the same file. Throw in a bit of Flow in the mix and it all goes to hell.
//
// See packages/helloworld/cli.js for an example of how to swap this out in the monorepo.
if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
require('../../../scripts/build/babel-register').registerForMonorepo();
}

module.exports = require('./index.flow.js');
6 changes: 1 addition & 5 deletions packages/core-cli-utils/src/private/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@ type AndroidBuild = {
gradleArgs?: Array<string>,
};

async function gradle(
cwd: string,
...args: string[]
): ReturnType<typeof execa> {
function gradle(cwd: string, ...args: string[]): ExecaPromise {
const gradlew = isWindows ? 'gradlew.bat' : './gradlew';
// $FlowFixMe[incompatible-return] Mismatch between flow and TypeScript types
return execa(gradlew, args, {
cwd,
stdio: 'inherit',
Expand Down
1 change: 1 addition & 0 deletions packages/core-cli-utils/src/private/apple.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type AppleInstallApp = {
// `xcrun simctl list devices`
device: string,
appPath: string,
bundleId: string,
...AppleOptions,
};

Expand Down
1 change: 1 addition & 0 deletions packages/core-cli-utils/src/private/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type PathCheckResult = {
export function isOnPath(dep: string, description: string): PathCheckResult {
const cmd = isWindows ? ['where', [dep]] : ['command', ['-v', dep]];
try {
const args = isWindows ? ['where', [dep]] : ['command', ['-v', dep]];
return {
dep,
description,
Expand Down
23 changes: 2 additions & 21 deletions packages/helloworld/.gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# OSX
#
.DS_Store

# Xcode
#
build/
*.pbxuser
!default.pbxuser
Expand All @@ -23,7 +21,6 @@ DerivedData
**/.xcode.env.local

# Android/IntelliJ
#
build/
.idea
.gradle
Expand All @@ -35,36 +32,20 @@ local.properties
!debug.keystore

# node.js
#
node_modules/
npm-debug.log
yarn-error.log

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/

**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundle artifact
*.jsbundle

# Ruby / CocoaPods
**/Pods/
/vendor/bundle/
ios/Pods/
vendor/bundle/

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

# testing
/coverage

# Yarn
.yarn/*
!.yarn/patches
Expand Down
66 changes: 59 additions & 7 deletions packages/helloworld/cli.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import type {ExecaPromise, Result} from 'execa';
import type {TaskSpec} from 'listr';

import {apple} from '@react-native/core-cli-utils';
import chalk from 'chalk';
import {program} from 'commander';
import debug from 'debug';
import execa from 'execa';
import fs from 'fs';
import Listr from 'listr';
import path from 'path';
import {Observable} from 'rxjs';
import chalk from 'chalk';
import debug from 'debug';

const log = debug('cli');

Expand Down Expand Up @@ -106,21 +106,23 @@ type IOSDevice = {
name: string,
};

function observe(result: ExecaPromise): Observable<string> {
type ExecaPromiseMetaized = Promise<Result> & child_process$ChildProcess;

function observe(result: ExecaPromiseMetaized): Observable<string> {
return new Observable(observer => {
result.stderr.on('data', data =>
data
.toString('utf8')
.split('\n')
.filter(line => line.length > 0)
.forEach(line => observer.next('🟢 ' + line)),
.forEach(line => observer.next('🟢 ' + line.trim())),
);
result.stdout.on('data', data =>
data
.toString('utf8')
.split('\n')
.filter(line => line.length > 0)
.forEach(line => observer.next('🟠 ' + line)),
.forEach(line => observer.next('🟠 ' + line.trim())),
);
for (const event of ['close', 'end']) {
result.stdout.on(event, () => observer.complete());
Expand All @@ -135,6 +137,26 @@ function observe(result: ExecaPromise): Observable<string> {
});
}

function getXcodeBuildSettings(iosProjectFolder: string) {
const {stdout} = execa.sync(
'xcodebuild',
[
'-workspace',
'HelloWorld.xcworkspace',
'-scheme',
'HelloWorld',
'-configuration',
'Debug',
'-sdk',
'iphonesimulator',
'-showBuildSettings',
'-json',
],
{cwd: iosProjectFolder},
);
return JSON.parse(stdout);
}

async function getSimulatorDetails(nameOrUDID: string): Promise<IOSDevice> {
const {stdout} = execa.sync('xcrun', [
'simctl',
Expand Down Expand Up @@ -229,7 +251,9 @@ function run(
title: task.label,
task: () => {
const action = task.action();
return action != null ? observe(action) : action;
if (action != null) {
return observe(action);
}
},
}));
return new Listr(spec).run();
Expand Down Expand Up @@ -274,10 +298,38 @@ build
}),
);

// TODO: Install
const settings = {
appPath: '',
bundleId: '',
};

await run({
buildSettings: {
order: 1,
label: 'Getting your build settings',
action: (): void => {
const xcode = getXcodeBuildSettings(cwd.ios)[0].buildSettings;
settings.appPath = path.join(
xcode.TARGET_BUILD_DIR,
xcode.EXECUTABLE_FOLDER_PATH,
);
settings.bundleId = xcode.PRODUCT_BUNDLE_IDENTIFIER;
},
},
});

await run(
apple.ios.install({
cwd: cwd.ios,
device: device.udid,
appPath: settings.appPath,
bundleId: settings.bundleId,
}),
);
});

if (require.main === module) {
program.parse();
}

export default program;
23 changes: 22 additions & 1 deletion packages/helloworld/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,39 @@
*/

/*::
import {cleanup} from 'BeatSaberBaseMeasurements';
import {Command} from 'commander';
*/

// eslint-disable-next-line lint/sort-imports
const {patchCoreCLIUtilsPackageJSON} = require('./scripts/monorepo');

function injectCoreCLIUtilsRuntimePatch() {
patchCoreCLIUtilsPackageJSON(true);
const cleared = {
status: false,
};
['exit', 'SIGUSR1', 'SIGUSR2', 'uncaughtException'].forEach(event => {
if (cleared.status) {
return;
}
patchCoreCLIUtilsPackageJSON(false);
cleared.status = true;
});
}

if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
// $FlowFixMe[cannot-resolve-module]
require('../../scripts/build/babel-register').registerForMonorepo();
}

injectCoreCLIUtilsRuntimePatch();

const program /*: Command */ = require('./cli.flow.js').default;

if (require.main === module) {
program.parse();
}

export default program;
module.exports = program;
19 changes: 18 additions & 1 deletion packages/helloworld/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,30 @@
*/

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const path = require('path');

/**
* Metro configuration
* https://reactnative.dev/docs/metro
*
* @type {import('metro-config').MetroConfig}
*/
const config = {};
const config = {
// Make Metro able to resolve required external dependencies
watchFolders: [
path.resolve(__dirname, '../../node_modules'),
path.resolve(__dirname, '../assets'),
path.resolve(__dirname, '../normalize-color'),
path.resolve(__dirname, '../polyfills'),
path.resolve(__dirname, '../react-native'),
path.resolve(__dirname, '../virtualized-lists'),
],
resolver: {
blockList: [/..\/react-native\/sdks\/hermes/],
extraNodeModules: {
'react-native': path.resolve(__dirname, '../react-native'),
},
},
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

0 comments on commit cf15e6b

Please sign in to comment.