diff --git a/docs/generated/packages/expo/executors/sync-deps.json b/docs/generated/packages/expo/executors/sync-deps.json index dec711b194..cbb07b21ae 100644 --- a/docs/generated/packages/expo/executors/sync-deps.json +++ b/docs/generated/packages/expo/executors/sync-deps.json @@ -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": [] diff --git a/docs/generated/packages/react-native/executors/sync-deps.json b/docs/generated/packages/react-native/executors/sync-deps.json index a832e8ee01..6eb033ee1e 100644 --- a/docs/generated/packages/react-native/executors/sync-deps.json +++ b/docs/generated/packages/react-native/executors/sync-deps.json @@ -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": [] diff --git a/e2e/react-native/src/react-native-legacy.test.ts b/e2e/react-native/src/react-native-legacy.test.ts index c86fdb5226..eae99ada9b 100644 --- a/e2e/react-native/src/react-native-legacy.test.ts +++ b/e2e/react-native/src/react-native-legacy.test.ts @@ -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': '*', }, }); }); diff --git a/packages/expo/src/executors/sync-deps/schema.d.ts b/packages/expo/src/executors/sync-deps/schema.d.ts index 1778132fee..1e4499bfa3 100644 --- a/packages/expo/src/executors/sync-deps/schema.d.ts +++ b/packages/expo/src/executors/sync-deps/schema.d.ts @@ -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 } diff --git a/packages/expo/src/executors/sync-deps/schema.json b/packages/expo/src/executors/sync-deps/schema.json index 5ba21b496d..42ad4ef386 100644 --- a/packages/expo/src/executors/sync-deps/schema.json +++ b/packages/expo/src/executors/sync-deps/schema.json @@ -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 } } } diff --git a/packages/expo/src/executors/sync-deps/sync-deps.impl.ts b/packages/expo/src/executors/sync-deps/sync-deps.impl.ts index 0c17f5d97d..b79d4eeaa5 100644 --- a/packages/expo/src/executors/sync-deps/sync-deps.impl.ts +++ b/packages/expo/src/executors/sync-deps/sync-deps.impl.ts @@ -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 { 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 { - 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; diff --git a/packages/expo/src/executors/update/update.impl.ts b/packages/expo/src/executors/update/update.impl.ts index bec9621d68..d5efb8a3f4 100644 --- a/packages/expo/src/executors/update/update.impl.ts +++ b/packages/expo/src/executors/update/update.impl.ts @@ -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 { 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 { diff --git a/packages/expo/src/utils/find-all-npm-dependencies.spec.ts b/packages/expo/src/utils/find-all-npm-dependencies.spec.ts new file mode 100644 index 0000000000..3a8738180b --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.spec.ts @@ -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', + ]); +}); diff --git a/packages/expo/src/utils/find-all-npm-dependencies.ts b/packages/expo/src/utils/find-all-npm-dependencies.ts new file mode 100644 index 0000000000..13aaca84c4 --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.ts @@ -0,0 +1,35 @@ +import { ProjectGraph } from '@nx/devkit'; + +export function findAllNpmDependencies( + graph: ProjectGraph, + projectName: string, + list: string[] = [], + seen = new Set() +) { + // 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; +} diff --git a/packages/react-native/src/executors/storybook/storybook.impl.ts b/packages/react-native/src/executors/storybook/storybook.impl.ts index 6c398dd6e6..ed480ecefe 100644 --- a/packages/react-native/src/executors/storybook/storybook.impl.ts +++ b/packages/react-native/src/executors/storybook/storybook.impl.ts @@ -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', diff --git a/packages/react-native/src/executors/sync-deps/schema.d.ts b/packages/react-native/src/executors/sync-deps/schema.d.ts index c88982181e..b0c9260454 100644 --- a/packages/react-native/src/executors/sync-deps/schema.d.ts +++ b/packages/react-native/src/executors/sync-deps/schema.d.ts @@ -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 } diff --git a/packages/react-native/src/executors/sync-deps/schema.json b/packages/react-native/src/executors/sync-deps/schema.json index 3e685663d5..cd0f80a910 100644 --- a/packages/react-native/src/executors/sync-deps/schema.json +++ b/packages/react-native/src/executors/sync-deps/schema.json @@ -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 } } } diff --git a/packages/react-native/src/executors/sync-deps/sync-deps.impl.ts b/packages/react-native/src/executors/sync-deps/sync-deps.impl.ts index e4d47194fd..dbb7dcde76 100644 --- a/packages/react-native/src/executors/sync-deps/sync-deps.impl.ts +++ b/packages/react-native/src/executors/sync-deps/sync-deps.impl.ts @@ -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 { - 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; diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index e4d37ae2bd..b35f6198df 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -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( - host, - workspacePackageJsonPath - ); + const workspacePackageJson = readJson(host, 'package.json'); const projectPackageJson = readJson( host, projectPackageJsonPath ); await syncDeps( + options.name, projectPackageJson, projectPackageJsonPath, workspacePackageJson diff --git a/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts b/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts new file mode 100644 index 0000000000..3a8738180b --- /dev/null +++ b/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts @@ -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', + ]); +}); diff --git a/packages/react-native/src/utils/find-all-npm-dependencies.ts b/packages/react-native/src/utils/find-all-npm-dependencies.ts new file mode 100644 index 0000000000..13aaca84c4 --- /dev/null +++ b/packages/react-native/src/utils/find-all-npm-dependencies.ts @@ -0,0 +1,35 @@ +import { ProjectGraph } from '@nx/devkit'; + +export function findAllNpmDependencies( + graph: ProjectGraph, + projectName: string, + list: string[] = [], + seen = new Set() +) { + // 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; +}