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" }, "items": { "type": "string" },
"default": [], "default": [],
"description": "An array of npm packages to exclude." "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": [] "presets": []

View File

@ -22,6 +22,11 @@
"items": { "type": "string" }, "items": { "type": "string" },
"default": [], "default": [],
"description": "An array of npm packages to exclude." "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": [] "presets": []

View File

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

View File

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

View File

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

View File

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

View File

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