Skip to content

Commit

Permalink
feat(react-native): add support for createNodes in react native (nrwl…
Browse files Browse the repository at this point in the history
  • Loading branch information
xiongemi authored Jan 8, 2024
1 parent ce81133 commit 2b652a4
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 4 deletions.
73 changes: 73 additions & 0 deletions e2e/react-native/src/react-native-pcv3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ChildProcess } from 'child_process';
import {
runCLI,
cleanupProject,
newProject,
uniq,
readJson,
runCommandUntil,
killProcessAndPorts,
fileExists,
} from 'e2e/utils';

describe('@nx/react-native/plugin', () => {
let appName: string;

beforeAll(() => {
newProject();
appName = uniq('app');
runCLI(
`generate @nx/react-native:app ${appName} --project-name-and-root-format=as-provided --no-interactive`,
{ env: { NX_PCV3: 'true' } }
);
});

afterAll(() => cleanupProject());

it('nx.json should contain plugin configuration', () => {
const nxJson = readJson('nx.json');
const reactNativePlugin = nxJson.plugins.find(
(plugin) => plugin.plugin === '@nx/react-native/plugin'
);
expect(reactNativePlugin).toBeDefined();
expect(reactNativePlugin.options).toBeDefined();
expect(reactNativePlugin.options.bundleTargetName).toEqual('bundle');
expect(reactNativePlugin.options.startTargetName).toEqual('start');
});

it('should bundle the app', async () => {
const result = runCLI(
`bundle ${appName} --platform=ios --bundle-output=dist.js --entry-file=src/main.tsx`
);
fileExists(` ${appName}/dist.js`);

expect(result).toContain(
`Successfully ran target bundle for project ${appName}`
);
}, 200_000);

it('should start the app', async () => {
let process: ChildProcess;
const port = 8081;

try {
process = await runCommandUntil(
`start ${appName} --no-interactive --port=${port}`,
(output) => {
return (
output.includes(`http://localhost:${port}`) ||
output.includes('Starting JS server...') ||
output.includes('Welcome to Metro')
);
}
);
} catch (err) {
console.error(err);
}

// port and process cleanup
if (process && process.pid) {
await killProcessAndPorts(process.pid, port);
}
});
});
5 changes: 3 additions & 2 deletions e2e/react-native/src/react-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ describe('react native', () => {
`start ${appName} --interactive=false --port=${port}`,
(output) => {
return (
output.includes(`Packager is ready at http://localhost::${port}`) ||
output.includes('Starting JS server...')
output.includes(`http://localhost:${port}`) ||
output.includes('Starting JS server...') ||
output.includes('Welcome to Metro')
);
}
);
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createNodes, ReactNativePluginOptions } from './plugins/plugin';
200 changes: 200 additions & 0 deletions packages/react-native/plugins/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
detectPackageManager,
NxJsonConfiguration,
readJsonFile,
TargetConfiguration,
writeJsonFile,
} from '@nx/devkit';
import { dirname, join } from 'path';
import { getLockFileName } from '@nx/js';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync } from 'fs';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';

export interface ReactNativePluginOptions {
startTargetName?: string;
runIosTargetName?: string;
runAndroidTargetName?: string;
buildIosTargetName?: string;
buildAndroidTargetName?: string;
bundleTargetName?: string;
}

const cachePath = join(projectGraphCacheDirectory, 'react-native.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};

const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};

function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration<ReactNativePluginOptions>>
> {
return readJsonFile(cachePath);
}

function writeTargetsToCache(
targets: Record<
string,
Record<string, TargetConfiguration<ReactNativePluginOptions>>
>
) {
writeJsonFile(cachePath, targets);
}

export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};

export const createNodes: CreateNodes<ReactNativePluginOptions> = [
'**/app.{json,config.js}',
(configFilePath, options, context) => {
options = normalizeOptions(options);
const projectRoot = dirname(configFilePath);

// Do not create a project if package.json or project.json or metro.config.js isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') ||
!siblingFiles.includes('project.json') ||
!siblingFiles.includes('metro.config.js')
) {
return {};
}
const appConfig = getAppConfig(configFilePath, context);
if (appConfig.expo) {
return {};
}

const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);

const targets = targetsCache[hash]
? targetsCache[hash]
: buildReactNativeTargets(projectRoot, options, context);

calculatedTargets[hash] = targets;

return {
projects: {
[projectRoot]: {
targets,
},
},
};
},
];

function buildReactNativeTargets(
projectRoot: string,
options: ReactNativePluginOptions,
context: CreateNodesContext
) {
const namedInputs = getNamedInputs(projectRoot, context);

const targets: Record<string, TargetConfiguration> = {
[options.startTargetName]: {
command: `react-native start`,
options: { cwd: projectRoot },
},
[options.runIosTargetName]: {
command: `react-native run-ios`,
options: { cwd: projectRoot },
},
[options.runAndroidTargetName]: {
command: `react-native run-android`,
options: { cwd: projectRoot },
},
[options.buildIosTargetName]: {
command: `react-native build-ios`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.buildIosTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'ios/build/Build/Products')],
},
[options.buildAndroidTargetName]: {
command: `react-native build-android`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.buildAndroidTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'android/app/build/outputs')],
},
[options.bundleTargetName]: {
command: `react-native bundle`,
options: { cwd: projectRoot },
dependsOn: [`^${options.bundleTargetName}`],
inputs: getInputs(namedInputs),
},
};

return targets;
}

function getAppConfig(
configFilePath: string,
context: CreateNodesContext
): any {
const resolvedPath = join(context.workspaceRoot, configFilePath);

let module = load(resolvedPath);
return module.default ?? module;
}

function getInputs(
namedInputs: NxJsonConfiguration['namedInputs']
): TargetConfiguration['inputs'] {
return [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{
externalDependencies: ['react-native'],
},
];
}

function getOutputs(projectRoot: string, dir: string) {
if (projectRoot === '.') {
return `{projectRoot}/${dir}`;
} else {
return `{workspaceRoot}/${projectRoot}/${dir}`;
}
}

/**
* Load the module after ensuring that the require cache is cleared.
*/
function load(path: string): any {
// Clear cache if the path is in the cache
if (require.cache[path]) {
for (const k of Object.keys(require.cache)) {
delete require.cache[k];
}
}

// Then require
return require(path);
}

function normalizeOptions(
options: ReactNativePluginOptions
): ReactNativePluginOptions {
options ??= {};
options.startTargetName ??= 'start';
options.runIosTargetName ??= 'run-ios';
options.runAndroidTargetName ??= 'run-android';
options.buildIosTargetName ??= 'build-ios';
options.buildAndroidTargetName ??= 'build-android';
options.bundleTargetName ??= 'bundle';
return options;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ export async function reactNativeApplicationGeneratorInternal(
): Promise<GeneratorCallback> {
const options = await normalizeOptions(host, schema);

const initTask = await initGenerator(host, { ...options, skipFormat: true });

createApplicationFiles(host, options);
addProject(host, options);

const initTask = await initGenerator(host, { ...options, skipFormat: true });
const lintTask = await addLinting(host, {
...options,
projectRoot: options.appProjectRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import {
addProjectConfiguration,
ProjectConfiguration,
readNxJson,
TargetConfiguration,
Tree,
} from '@nx/devkit';
import { NormalizedSchema } from './normalize-options';

export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
const hasPlugin = nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/react-native/plugin'
: p.plugin === '@nx/react-native/plugin'
);

const project: ProjectConfiguration = {
root: options.appProjectRoot,
sourceRoot: `${options.appProjectRoot}/src`,
projectType: 'application',
targets: { ...getTargets(options) },
targets: hasPlugin ? {} : getTargets(options),
tags: options.parsedTags,
};

Expand Down
35 changes: 35 additions & 0 deletions packages/react-native/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
detectPackageManager,
formatFiles,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { Schema } from './schema';

Expand Down Expand Up @@ -39,6 +41,7 @@ import {
} from '../../utils/versions';

import { addGitIgnoreEntry } from './lib/add-git-ignore-entry';
import { ReactNativePluginOptions } from '../../../plugins/plugin';

export async function reactNativeInitGenerator(host: Tree, schema: Schema) {
addGitIgnoreEntry(host);
Expand Down Expand Up @@ -71,6 +74,10 @@ export async function reactNativeInitGenerator(host: Tree, schema: Schema) {
tasks.push(detoxTask);
}

if (process.env.NX_PCV3 === 'true') {
addPlugin(host);
}

if (!schema.skipFormat) {
await formatFiles(host);
}
Expand Down Expand Up @@ -123,4 +130,32 @@ function moveDependency(host: Tree) {
return removeDependenciesFromPackageJson(host, ['@nx/react-native'], []);
}

function addPlugin(host: Tree) {
const nxJson = readNxJson(host);
nxJson.plugins ??= [];

for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/react-native/plugin'
: plugin.plugin === '@nx/react-native/plugin'
) {
return;
}
}

nxJson.plugins.push({
plugin: '@nx/react-native/plugin',
options: {
startTargetName: 'start',
bundleTargetName: 'bundle',
runIosTargetName: 'run-ios',
runAndroidTargetName: 'run-android',
buildIosTargetName: 'build-ios',
buildAndroidTargetName: 'build-android',
} as ReactNativePluginOptions,
});
updateNxJson(host, nxJson);
}

export default reactNativeInitGenerator;

0 comments on commit 2b652a4

Please sign in to comment.