fix(react-native): add all flag to sync-deps (#21821)

This commit is contained in:
Emily Xiong 2024-02-16 01:12:16 -05:00 committed by GitHub
parent 27cf3082da
commit 0f0074c91f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 411 additions and 39 deletions

View File

@ -22,6 +22,11 @@
"items": { "type": "string" },
"default": [],
"description": "An array of npm packages to exclude."
},
"all": {
"type": "boolean",
"description": "Copy all dependencies and devDependencies from the workspace root package.json.",
"default": false
}
},
"presets": []

View File

@ -22,6 +22,11 @@
"items": { "type": "string" },
"default": [],
"description": "An array of npm packages to exclude."
},
"all": {
"type": "boolean",
"description": "Copy all dependencies and devDependencies from the workspace root package.json.",
"default": false
}
},
"presets": []

View File

@ -215,18 +215,34 @@ describe('@nx/react-native (legacy)', () => {
return `import AsyncStorage from '@react-native-async-storage/async-storage';${content}`;
});
await runCLIAsync(`sync-deps ${appName}`);
let result = readJson(join('apps', appName, 'package.json'));
expect(result).toMatchObject({
dependencies: {
'@react-native-async-storage/async-storage': '*',
},
});
await runCLIAsync(
`sync-deps ${appName} --include=react-native-image-picker`
);
const result = readJson(join('apps', appName, 'package.json'));
result = readJson(join('apps', appName, 'package.json'));
expect(result).toMatchObject({
dependencies: {
'@react-native-async-storage/async-storage': '*',
'react-native-image-picker': '*',
},
});
await runCLIAsync(`sync-deps ${appName} --all`);
result = readJson(join('apps', appName, 'package.json'));
expect(result).toMatchObject({
dependencies: {
'@react-native-async-storage/async-storage': '*',
'react-native-image-picker': '*',
'react-native': '*',
},
devDependencies: {
'@react-native-async-storage/async-storage': '*',
'@nx/react-native': '*',
},
});
});

View File

@ -1,4 +1,5 @@
export interface ExpoSyncDepsOptions {
include: string[] | string; // default is an empty array []
exclude: string[] | string; // default is an empty array []
all: boolean; // default is false
}

View File

@ -23,6 +23,11 @@
},
"default": [],
"description": "An array of npm packages to exclude."
},
"all": {
"type": "boolean",
"description": "Copy all dependencies and devDependencies from the workspace root package.json.",
"default": false
}
}
}

View File

@ -2,12 +2,16 @@ import { join } from 'path';
import * as chalk from 'chalk';
import {
ExecutorContext,
ProjectGraph,
logger,
readCachedProjectGraph,
readJsonFile,
writeJsonFile,
} from '@nx/devkit';
import { ExpoSyncDepsOptions } from './schema';
import { findAllNpmDependencies } from '../../utils/find-all-npm-dependencies';
import { PackageJson } from 'nx/src/utils/package-json';
export interface ReactNativeSyncDepsOutput {
success: boolean;
@ -19,17 +23,31 @@ export default async function* syncDepsExecutor(
): AsyncGenerator<ReactNativeSyncDepsOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const workspacePackageJsonPath = join(context.root, 'package.json');
const projectPackageJsonPath = join(
context.root,
projectRoot,
'package.json'
);
const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
const projectPackageJson = readJsonFile(projectPackageJsonPath);
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
projectRoot,
context.root,
context.projectName,
projectPackageJson,
projectPackageJsonPath,
workspacePackageJson,
context.projectGraph,
typeof options.include === 'string'
? options.include.split(',')
: options.include,
typeof options.exclude === 'string'
? options.exclude.split(',')
: options.exclude
: options.exclude,
options.all
)
);
@ -37,23 +55,27 @@ export default async function* syncDepsExecutor(
}
export async function syncDeps(
projectRoot: string,
workspaceRoot: string,
projectName: string,
projectPackageJson: PackageJson,
projectPackageJsonPath: string,
workspacePackageJson: PackageJson,
projectGraph: ProjectGraph = readCachedProjectGraph(),
include: string[] = [],
exclude: string[] = []
exclude: string[] = [],
all: boolean = false
): Promise<string[]> {
const workspacePackageJsonPath = join(workspaceRoot, 'package.json');
const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
let npmDeps = Object.keys(workspacePackageJson.dependencies || {});
let npmDevdeps = Object.keys(workspacePackageJson.devDependencies || {});
let npmDeps = all
? Object.keys(workspacePackageJson.dependencies || {})
: findAllNpmDependencies(projectGraph, projectName);
let npmDevdeps = all
? Object.keys(workspacePackageJson.devDependencies || {})
: [];
const packageJsonPath = join(workspaceRoot, projectRoot, 'package.json');
const packageJson = readJsonFile(packageJsonPath);
const newDeps = [];
let updated = false;
if (!packageJson.dependencies) {
packageJson.dependencies = {};
if (!projectPackageJson.dependencies) {
projectPackageJson.dependencies = {};
updated = true;
}
@ -64,30 +86,36 @@ export async function syncDeps(
npmDeps = npmDeps.filter((dep) => !exclude.includes(dep));
}
if (!packageJson.devDependencies) {
packageJson.devDependencies = {};
if (!projectPackageJson.devDependencies) {
projectPackageJson.devDependencies = {};
}
if (!packageJson.dependencies) {
packageJson.dependencies = {};
if (!projectPackageJson.dependencies) {
projectPackageJson.dependencies = {};
}
npmDeps.forEach((dep) => {
if (!packageJson.dependencies[dep] && !packageJson.devDependencies[dep]) {
packageJson.dependencies[dep] = '*';
if (
!projectPackageJson.dependencies[dep] &&
!projectPackageJson.devDependencies[dep]
) {
projectPackageJson.dependencies[dep] = '*';
newDeps.push(dep);
updated = true;
}
});
npmDevdeps.forEach((dep) => {
if (!packageJson.dependencies[dep] && !packageJson.devDependencies[dep]) {
packageJson.devDependencies[dep] = '*';
if (
!projectPackageJson.dependencies[dep] &&
!projectPackageJson.devDependencies[dep]
) {
projectPackageJson.devDependencies[dep] = '*';
newDeps.push(dep);
updated = true;
}
});
if (updated) {
writeJsonFile(packageJsonPath, packageJson);
writeJsonFile(projectPackageJsonPath, projectPackageJson);
}
return newDeps;

View File

@ -1,5 +1,5 @@
import { ExecutorContext, names } from '@nx/devkit';
import { resolve as pathResolve } from 'path';
import { ExecutorContext, names, readJsonFile } from '@nx/devkit';
import { join, resolve as pathResolve } from 'path';
import { ChildProcess, fork } from 'child_process';
import { resolveEas } from '../../utils/resolve-eas';
@ -23,10 +23,27 @@ export default async function* buildExecutor(
): AsyncGenerator<ReactNativeUpdateOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const workspacePackageJsonPath = join(context.root, 'package.json');
const projectPackageJsonPath = join(
context.root,
projectRoot,
'package.json'
);
const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
const projectPackageJson = readJsonFile(projectPackageJsonPath);
await installAsync(context.root, { packages: ['expo-updates'] });
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(projectRoot, context.root, ['expo-updates'])
await syncDeps(
context.projectName,
projectPackageJson,
projectPackageJsonPath,
workspacePackageJson,
context.projectGraph,
['expo-updates']
)
);
try {

View File

@ -0,0 +1,103 @@
import { findAllNpmDependencies } from './find-all-npm-dependencies';
import { DependencyType, ProjectGraph } from '@nx/devkit';
test('findAllNpmDependencies', () => {
const graph: ProjectGraph = {
nodes: {
myapp: {
type: 'app',
name: 'myapp',
data: { files: [] },
},
lib1: {
type: 'lib',
name: 'lib1',
data: { files: [] },
},
lib2: {
type: 'lib',
name: 'lib2',
data: { files: [] },
},
lib3: {
type: 'lib',
name: 'lib3',
data: { files: [] },
},
} as any,
externalNodes: {
'npm:react-native-image-picker': {
type: 'npm',
name: 'npm:react-native-image-picker',
data: {
version: '1',
packageName: 'react-native-image-picker',
},
},
'npm:react-native-dialog': {
type: 'npm',
name: 'npm:react-native-dialog',
data: {
version: '1',
packageName: 'react-native-dialog',
},
},
'npm:react-native-snackbar': {
type: 'npm',
name: 'npm:react-native-snackbar',
data: {
version: '1',
packageName: 'react-native-snackbar',
},
},
'npm:@nx/react-native': {
type: 'npm',
name: 'npm:@nx/react-native',
data: {
version: '1',
packageName: '@nx/react-native',
},
},
},
dependencies: {
myapp: [
{ type: DependencyType.static, source: 'myapp', target: 'lib1' },
{ type: DependencyType.static, source: 'myapp', target: 'lib2' },
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:react-native-image-picker',
},
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:@nx/react-native',
},
],
lib1: [
{ type: DependencyType.static, source: 'lib1', target: 'lib2' },
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-snackbar',
},
],
lib2: [{ type: DependencyType.static, source: 'lib2', target: 'lib3' }],
lib3: [
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-dialog',
},
],
},
};
const result = findAllNpmDependencies(graph, 'myapp');
expect(result).toEqual([
'react-native-dialog',
'react-native-snackbar',
'react-native-image-picker',
]);
});

View File

@ -0,0 +1,35 @@
import { ProjectGraph } from '@nx/devkit';
export function findAllNpmDependencies(
graph: ProjectGraph,
projectName: string,
list: string[] = [],
seen = new Set<string>()
) {
// In case of bad circular dependencies
if (seen.has(projectName)) {
return list;
}
seen.add(projectName);
const node = graph.externalNodes[projectName];
// Don't want to include '@nx/react-native' and '@nx/expo' because React Native
// autolink will warn that the package has no podspec file for iOS.
if (node) {
if (
node.name !== `npm:@nx/react-native` &&
node.name !== `npm:@nrwl/react-native` &&
node.name !== `npm:@nx/expo` &&
node.name !== `npm:@nrwl/expo`
) {
list.push(node.data.packageName);
}
} else {
// it's workspace project, search for it's dependencies
graph.dependencies[projectName]?.forEach((dep) =>
findAllNpmDependencies(graph, dep.target, list, seen)
);
}
return list;
}

View File

@ -41,9 +41,11 @@ export default async function* reactNativeStorybookExecutor(
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
context.projectName,
projectPackageJson,
packageJsonPath,
workspacePackageJson,
context.projectGraph,
[
`@storybook/react-native`,
'@storybook/addon-ondevice-actions',

View File

@ -1,4 +1,5 @@
export interface ReactNativeSyncDepsOptions {
include: string[] | string; // default is an empty array []
exclude: string[] | string; // default is an empty array []
all: boolean; // default is false
}

View File

@ -23,6 +23,11 @@
},
"default": [],
"description": "An array of npm packages to exclude."
},
"all": {
"type": "boolean",
"description": "Copy all dependencies and devDependencies from the workspace root package.json.",
"default": false
}
}
}

View File

@ -2,12 +2,15 @@ import { join } from 'path';
import * as chalk from 'chalk';
import {
ExecutorContext,
ProjectGraph,
logger,
readCachedProjectGraph,
readJsonFile,
writeJsonFile,
} from '@nx/devkit';
import { ReactNativeSyncDepsOptions } from './schema';
import { findAllNpmDependencies } from '../../utils/find-all-npm-dependencies';
import { PackageJson } from 'nx/src/utils/package-json';
export interface ReactNativeSyncDepsOutput {
@ -33,15 +36,18 @@ export default async function* syncDepsExecutor(
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
context.projectName,
projectPackageJson,
projectPackageJsonPath,
workspacePackageJson,
context.projectGraph,
typeof options.include === 'string'
? options.include.split(',')
: options.include,
typeof options.exclude === 'string'
? options.exclude.split(',')
: options.exclude
: options.exclude,
options.all
)
);
@ -49,14 +55,21 @@ export default async function* syncDepsExecutor(
}
export async function syncDeps(
projectName: string,
projectPackageJson: PackageJson,
projectPackageJsonPath: string,
workspacePackageJson: PackageJson,
projectGraph: ProjectGraph = readCachedProjectGraph(),
include: string[] = [],
exclude: string[] = []
exclude: string[] = [],
all: boolean = false
): Promise<string[]> {
let npmDeps = Object.keys(workspacePackageJson.dependencies || {});
let npmDevdeps = Object.keys(workspacePackageJson.devDependencies || {});
let npmDeps = all
? Object.keys(workspacePackageJson.dependencies || {})
: findAllNpmDependencies(projectGraph, projectName);
let npmDevdeps = all
? Object.keys(workspacePackageJson.devDependencies || {})
: [];
const newDeps = [];
let updated = false;

View File

@ -3,6 +3,7 @@ import {
GeneratorCallback,
joinPathFragments,
output,
readCachedProjectGraph,
readJson,
runTasksInSerial,
Tree,
@ -98,22 +99,19 @@ export async function reactNativeApplicationGeneratorInternal(
joinPathFragments(host.root, options.iosProjectRoot)
);
if (options.install) {
const workspacePackageJsonPath = joinPathFragments('package.json');
const projectPackageJsonPath = joinPathFragments(
options.appProjectRoot,
'package.json'
);
const workspacePackageJson = readJson<PackageJson>(
host,
workspacePackageJsonPath
);
const workspacePackageJson = readJson<PackageJson>(host, 'package.json');
const projectPackageJson = readJson<PackageJson>(
host,
projectPackageJsonPath
);
await syncDeps(
options.name,
projectPackageJson,
projectPackageJsonPath,
workspacePackageJson

View File

@ -0,0 +1,103 @@
import { findAllNpmDependencies } from './find-all-npm-dependencies';
import { DependencyType, ProjectGraph } from '@nx/devkit';
test('findAllNpmDependencies', () => {
const graph: ProjectGraph = {
nodes: {
myapp: {
type: 'app',
name: 'myapp',
data: { files: [] },
},
lib1: {
type: 'lib',
name: 'lib1',
data: { files: [] },
},
lib2: {
type: 'lib',
name: 'lib2',
data: { files: [] },
},
lib3: {
type: 'lib',
name: 'lib3',
data: { files: [] },
},
} as any,
externalNodes: {
'npm:react-native-image-picker': {
type: 'npm',
name: 'npm:react-native-image-picker',
data: {
version: '1',
packageName: 'react-native-image-picker',
},
},
'npm:react-native-dialog': {
type: 'npm',
name: 'npm:react-native-dialog',
data: {
version: '1',
packageName: 'react-native-dialog',
},
},
'npm:react-native-snackbar': {
type: 'npm',
name: 'npm:react-native-snackbar',
data: {
version: '1',
packageName: 'react-native-snackbar',
},
},
'npm:@nx/react-native': {
type: 'npm',
name: 'npm:@nx/react-native',
data: {
version: '1',
packageName: '@nx/react-native',
},
},
},
dependencies: {
myapp: [
{ type: DependencyType.static, source: 'myapp', target: 'lib1' },
{ type: DependencyType.static, source: 'myapp', target: 'lib2' },
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:react-native-image-picker',
},
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:@nx/react-native',
},
],
lib1: [
{ type: DependencyType.static, source: 'lib1', target: 'lib2' },
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-snackbar',
},
],
lib2: [{ type: DependencyType.static, source: 'lib2', target: 'lib3' }],
lib3: [
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-dialog',
},
],
},
};
const result = findAllNpmDependencies(graph, 'myapp');
expect(result).toEqual([
'react-native-dialog',
'react-native-snackbar',
'react-native-image-picker',
]);
});

View File

@ -0,0 +1,35 @@
import { ProjectGraph } from '@nx/devkit';
export function findAllNpmDependencies(
graph: ProjectGraph,
projectName: string,
list: string[] = [],
seen = new Set<string>()
) {
// In case of bad circular dependencies
if (seen.has(projectName)) {
return list;
}
seen.add(projectName);
const node = graph.externalNodes[projectName];
// Don't want to include '@nx/react-native' and '@nx/expo' because React Native
// autolink will warn that the package has no podspec file for iOS.
if (node) {
if (
node.name !== `npm:@nx/react-native` &&
node.name !== `npm:@nrwl/react-native` &&
node.name !== `npm:@nx/expo` &&
node.name !== `npm:@nrwl/expo`
) {
list.push(node.data.packageName);
}
} else {
// it's workspace project, search for it's dependencies
graph.dependencies[projectName]?.forEach((dep) =>
findAllNpmDependencies(graph, dep.target, list, seen)
);
}
return list;
}