Skip to content
Merged
10 changes: 10 additions & 0 deletions .changeset/little-rocks-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@rock-js/platform-apple-helpers': patch
'@rock-js/platform-android': patch
'@rock-js/plugin-repack': patch
'@rock-js/platform-ios': patch
'@rock-js/plugin-metro': patch
'@rock-js/config': patch
---

feat: allow running dev server from run commands with `--dev-server` flag
59 changes: 54 additions & 5 deletions packages/config/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ export type PluginOutput = {
description: string;
};

export type DevServerArgs = {
interactive: boolean;
clientLogs: boolean;
port?: string;
host?: string;
https?: boolean;
resetCache?: boolean;
devServer?: boolean;
platforms?: string[];
[key: string]: unknown;
};

export type StartDevServerArgs = {
root: string;
args: DevServerArgs;
reactNativeVersion: string;
reactNativePath: string;
platforms: Record<string, object>;
};

type StartDevServerFunction = (options: StartDevServerArgs) => Promise<void>;

export type BundlerPluginOutput = {
name: string;
description: string;
start: StartDevServerFunction;
};

export type PlatformOutput = PluginOutput & {
autolinkingConfig: { project: Record<string, unknown> | undefined };
};
Expand All @@ -27,10 +55,11 @@ export type PluginApi = {
null | undefined | (() => RemoteBuildCache)
>;
getFingerprintOptions: () => FingerprintSources;
getBundlerStart: () => ({ args }: { args: DevServerArgs }) => void;
};

type PluginType = (args: PluginApi) => PluginOutput;

type BundlerPluginType = (args: PluginApi) => BundlerPluginOutput;
type PlatformType = (args: PluginApi) => PlatformOutput;

type ArgValue = string | string[] | boolean;
Expand Down Expand Up @@ -63,7 +92,7 @@ export type ConfigType = {
root?: string;
reactNativeVersion?: string;
reactNativePath?: string;
bundler?: PluginType;
bundler?: BundlerPluginType;
plugins?: PluginType[];
platforms?: Record<string, PlatformType>;
commands?: Array<CommandType>;
Expand All @@ -79,6 +108,7 @@ export type ConfigOutput = {
root: string;
commands?: Array<CommandType>;
platforms?: Record<string, PlatformOutput>;
bundler?: BundlerPluginOutput;
} & PluginApi;

const extensions = ['.js', '.ts', '.mjs'];
Expand Down Expand Up @@ -160,6 +190,8 @@ export async function getConfig(
process.exit(1);
}

let bundler: BundlerPluginOutput | undefined;

const api = {
registerCommand: (command: CommandType) => {
validatedConfig.commands = [...(validatedConfig.commands || []), command];
Expand All @@ -186,6 +218,17 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro
},
getFingerprintOptions: () =>
validatedConfig.fingerprint as FingerprintSources,
getBundlerStart:
() =>
({ args }: { args: DevServerArgs }) => {
return bundler?.start({
root: api.getProjectRoot(),
args,
reactNativeVersion: api.getReactNativeVersion(),
reactNativePath: api.getReactNativePath(),
platforms: api.getPlatforms(),
});
},
};

const platforms: Record<string, PlatformOutput> = {};
Expand All @@ -205,7 +248,11 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro
}

if (validatedConfig.bundler) {
assignOriginToCommand(validatedConfig.bundler, api, validatedConfig);
bundler = assignOriginToCommand(
validatedConfig.bundler,
api,
validatedConfig,
) as BundlerPluginOutput;
}

for (const internalPlugin of internalPlugins) {
Expand All @@ -220,6 +267,7 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro
root: projectRoot,
commands: validatedConfig.commands ?? [],
platforms: platforms ?? {},
bundler,
...api,
};

Expand All @@ -236,16 +284,17 @@ function resolveReactNativePath(root: string) {
* Assigns __origin property to each command in the config for later use in error handling.
*/
function assignOriginToCommand(
plugin: PluginType,
plugin: PluginType | BundlerPluginType,
api: PluginApi,
config: ConfigType,
) {
const len = config.commands?.length ?? 0;
const { name } = plugin(api);
const { name, ...rest } = plugin(api);
const newlen = config.commands?.length ?? 0;
for (let i = len; i < newlen; i++) {
if (config.commands?.[i]) {
config.commands[i].__origin = name;
}
}
return { name, ...rest };
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const androidProject: AndroidProjectConfig = {
const OLD_ENV = process.env;
let adbDevicesCallsCount = 0;

const mockPlatforms = { ios: {}, android: {} };

beforeEach(() => {
adbDevicesCallsCount = 0;
vi.clearAllMocks();
Expand Down Expand Up @@ -312,9 +314,12 @@ test.each([['release'], ['debug'], ['staging']])(
'/',
undefined,
{ extraSources: [], ignorePaths: [], env: [] },
vi.fn(), // startDevServer mock
'/path/to/react-native', // reactNativePath
'0.79.0', // reactNativeVersion
mockPlatforms,
);

expect(tools.outro).toBeCalledWith('Success 🎉.');
expect(tools.logger.error).not.toBeCalled();

// Runs installDebug with only active architecture arm64-v8a
Expand Down Expand Up @@ -361,9 +366,12 @@ test('runAndroid runs gradle build with custom --appId, --appIdSuffix and --main
'/',
undefined,
{ extraSources: [], ignorePaths: [], env: [] },
vi.fn(), // startDevServer mock
'/path/to/react-native', // reactNativePath
'0.79.0', // reactNativeVersion
mockPlatforms,
);

expect(tools.outro).toBeCalledWith('Success 🎉.');
expect(logErrorSpy).not.toBeCalled();

// launches com.custom.suffix app with OtherActivity on emulator-5552
Expand All @@ -388,6 +396,10 @@ test('runAndroid fails to launch an app on not-connected device when specified w
'/',
undefined,
{ extraSources: [], ignorePaths: [], env: [] },
vi.fn(), // startDevServer mock
'/path/to/react-native', // reactNativePath
'0.79.0', // reactNativeVersion
mockPlatforms,
);
expect(logWarnSpy).toBeCalledWith(
'No devices or emulators found matching "emulator-5554". Using available one instead.',
Expand Down Expand Up @@ -457,6 +469,10 @@ test.each([['release'], ['debug']])(
'/',
undefined,
{ extraSources: [], ignorePaths: [], env: [] },
vi.fn(), // startDevServer mock
'/path/to/react-native', // reactNativePath
'0.79.0', // reactNativeVersion
mockPlatforms,
);

// we don't want to run installDebug when a device is selected, because gradle will install the app on all connected devices
Expand Down Expand Up @@ -513,11 +529,21 @@ test('runAndroid launches an app on all connected devices', async () => {
});
});

await runAndroid({ ...androidProject }, { ...args }, '/', undefined, {
extraSources: [],
ignorePaths: [],
env: [],
});
await runAndroid(
{ ...androidProject },
{ ...args },
'/',
undefined,
{
extraSources: [],
ignorePaths: [],
env: [],
},
vi.fn(),
'/path/to/react-native',
'0.79.0',
mockPlatforms,
);

// Runs assemble debug task with active architectures arm64-v8a, armeabi-v7a
expect(spawn).toBeCalledWith(
Expand Down Expand Up @@ -584,6 +610,10 @@ test('runAndroid skips building when --binary-path is passed', async () => {
'/root',
undefined,
{ extraSources: [], ignorePaths: [], env: [] },
vi.fn(), // startDevServer mock
'/path/to/react-native', // reactNativePath
'0.79.0', // reactNativeVersion
mockPlatforms,
);

// Skips gradle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AndroidProjectConfig } from '@react-native-community/cli-types';
import type { PluginApi } from '@rock-js/config';
import { intro, outro } from '@rock-js/tools';
import { getValidProjectConfig } from '../getValidProjectConfig.js';
import type { Flags } from './runAndroid.js';
import { runAndroid, runOptions } from './runAndroid.js';
Expand All @@ -13,6 +14,7 @@ export function registerRunCommand(
description:
'Builds your app and starts it on a connected Android emulator or a device.',
action: async (args) => {
intro('Running Android app');
const projectRoot = api.getProjectRoot();
const androidConfig = getValidProjectConfig(projectRoot, pluginConfig);
await runAndroid(
Expand All @@ -21,7 +23,12 @@ export function registerRunCommand(
projectRoot,
await api.getRemoteCacheProvider(),
api.getFingerprintOptions(),
api.getBundlerStart(),
api.getReactNativeVersion(),
api.getReactNativePath(),
api.getPlatforms(),
);
outro('Success 🎉.');
},
options: runOptions,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import type {
AndroidProjectConfig,
Config,
} from '@react-native-community/cli-types';
import type { StartDevServerArgs } from '@rock-js/config';
import type { FingerprintSources, RemoteBuildCache } from '@rock-js/tools';
import {
color,
formatArtifactName,
intro,
isInteractive,
logger,
outro,
promptSelect,
RockError,
spinner,
Expand All @@ -37,6 +36,8 @@ export interface Flags extends BuildFlags {
binaryPath?: string;
user?: string;
local?: boolean;
devServer?: boolean;
clientLogs?: boolean;
}

export type AndroidProject = NonNullable<Config['project']['android']>;
Expand All @@ -50,8 +51,26 @@ export async function runAndroid(
projectRoot: string,
remoteCacheProvider: null | (() => RemoteBuildCache) | undefined,
fingerprintOptions: FingerprintSources,
startDevServer: (options: StartDevServerArgs) => void,
reactNativeVersion: string,
reactNativePath: string,
platforms: { [platform: string]: object },
) {
intro('Running Android app');
const startDevServerHelper = () => {
if (args.devServer) {
logger.info('Starting dev server...');
startDevServer({
root: projectRoot,
reactNativePath,
reactNativeVersion,
platforms,
args: {
interactive: isInteractive(),
clientLogs: args.clientLogs ?? true,
},
});
}
};

normalizeArgs(args, projectRoot);

Expand Down Expand Up @@ -111,7 +130,7 @@ export async function runAndroid(
}
}

outro('Success 🎉.');
startDevServerHelper();
}

async function selectAndLaunchDevice() {
Expand Down Expand Up @@ -279,4 +298,13 @@ export const runOptions = [
name: '--user <number>',
description: 'Id of the User Profile you want to install the app on.',
},
{
name: '--client-logs',
description: 'Enable client logs in dev server.',
},
{
name: '--dev-server',
description:
'Automatically start a dev server (bundler) after building the app.',
},
];
Loading
Loading