From 274eb99d850cd61864ff22df36a74c7fecd6c20f Mon Sep 17 00:00:00 2001 From: Edouard Bozon Date: Thu, 29 Jul 2021 23:02:07 +0200 Subject: [PATCH] feat(devkit): make nx.json optional (#6398) * feat(devkit): make nx.json optional (#5678) Allow project configuration functions to work without nx.json configuration file, this is particulary handy for regular Angular CLI projects. * docs(devkit): make `NxJsonConfiguration` partial Co-authored-by: Jason Jean --- docs/angular/api-nx-devkit/index.md | 2 +- docs/node/api-nx-devkit/index.md | 2 +- docs/react/api-nx-devkit/index.md | 2 +- .../generators/project-configuration.spec.ts | 151 +++++++++++++++++- .../src/generators/project-configuration.ts | 97 ++++++----- .../devkit/src/utils/get-workspace-layout.ts | 19 ++- 6 files changed, 211 insertions(+), 62 deletions(-) diff --git a/docs/angular/api-nx-devkit/index.md b/docs/angular/api-nx-devkit/index.md index bd8348d3d8570a..7ca571eb7c0a39 100644 --- a/docs/angular/api-nx-devkit/index.md +++ b/docs/angular/api-nx-devkit/index.md @@ -482,7 +482,7 @@ Implementation of a target of a project that handles multiple projects to be bat ### WorkspaceConfiguration -Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../angular/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Omit`<[`NxJsonConfiguration`](../../angular/nx-devkit/index#nxjsonconfiguration), `"projects"`\> +Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../angular/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Partial`<`Omit`<[`NxJsonConfiguration`](../../angular/nx-devkit/index#nxjsonconfiguration), `"projects"`\>\> ## Variables diff --git a/docs/node/api-nx-devkit/index.md b/docs/node/api-nx-devkit/index.md index 23c108e91b6c31..bdd0cd996b44f0 100644 --- a/docs/node/api-nx-devkit/index.md +++ b/docs/node/api-nx-devkit/index.md @@ -482,7 +482,7 @@ Implementation of a target of a project that handles multiple projects to be bat ### WorkspaceConfiguration -Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../node/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Omit`<[`NxJsonConfiguration`](../../node/nx-devkit/index#nxjsonconfiguration), `"projects"`\> +Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../node/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Partial`<`Omit`<[`NxJsonConfiguration`](../../node/nx-devkit/index#nxjsonconfiguration), `"projects"`\>\> ## Variables diff --git a/docs/react/api-nx-devkit/index.md b/docs/react/api-nx-devkit/index.md index 753caee0bc9b50..45e780f17e735f 100644 --- a/docs/react/api-nx-devkit/index.md +++ b/docs/react/api-nx-devkit/index.md @@ -482,7 +482,7 @@ Implementation of a target of a project that handles multiple projects to be bat ### WorkspaceConfiguration -Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../react/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Omit`<[`NxJsonConfiguration`](../../react/nx-devkit/index#nxjsonconfiguration), `"projects"`\> +Ƭ **WorkspaceConfiguration**: `Omit`<[`WorkspaceJsonConfiguration`](../../react/nx-devkit/index#workspacejsonconfiguration), `"projects"`\> & `Partial`<`Omit`<[`NxJsonConfiguration`](../../react/nx-devkit/index#nxjsonconfiguration), `"projects"`\>\> ## Variables diff --git a/packages/devkit/src/generators/project-configuration.spec.ts b/packages/devkit/src/generators/project-configuration.spec.ts index c7fe14a2b14c20..62917260a8020c 100644 --- a/packages/devkit/src/generators/project-configuration.spec.ts +++ b/packages/devkit/src/generators/project-configuration.spec.ts @@ -1,17 +1,18 @@ import { Tree } from '@nrwl/tao/src/shared/tree'; +import { ProjectConfiguration } from '@nrwl/tao/src/shared/workspace'; + +import { createTreeWithEmptyWorkspace } from '../tests/create-tree-with-empty-workspace'; +import { readJson } from '../utils/json'; import { - readProjectConfiguration, addProjectConfiguration, - updateProjectConfiguration, - removeProjectConfiguration, getProjects, + readProjectConfiguration, readWorkspaceConfiguration, - WorkspaceConfiguration, + removeProjectConfiguration, + updateProjectConfiguration, updateWorkspaceConfiguration, + WorkspaceConfiguration, } from './project-configuration'; -import { createTreeWithEmptyWorkspace } from '../tests/create-tree-with-empty-workspace'; -import { ProjectConfiguration } from '@nrwl/tao/src/shared/workspace'; -import { readJson } from '../utils/json'; const baseTestProjectConfig: ProjectConfiguration = { root: 'libs/test', @@ -171,4 +172,140 @@ describe('project configuration', () => { expect(readJson(tree, 'nx.json').$schema).not.toBeDefined(); }); }); + + describe('without nx.json', () => { + beforeEach(() => tree.delete('nx.json')); + + afterEach(() => expect(tree.exists('nx.json')).toEqual(false)); + + it('should create project.json file when adding a project if standalone is true', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, true); + + expect(tree.exists('libs/test/project.json')).toBeTruthy(); + }); + + it('should create project.json file if all other apps in the workspace use project.json', () => { + addProjectConfiguration( + tree, + 'project-a', + { + root: 'apps/project-a', + targets: {}, + }, + true + ); + addProjectConfiguration( + tree, + 'project-b', + { + root: 'apps/project-b', + targets: {}, + }, + true + ); + expect(tree.exists('apps/project-b/project.json')).toBeTruthy(); + }); + + it("should not create project.json file if any other app in the workspace doesn't use project.json", () => { + addProjectConfiguration( + tree, + 'project-a', + { + root: 'apps/project-a', + targets: {}, + }, + false + ); + addProjectConfiguration( + tree, + 'project-b', + { + root: 'apps/project-b', + targets: {}, + }, + false + ); + expect(tree.exists('apps/project-a/project.json')).toBeFalsy(); + expect(tree.exists('apps/project-b/project.json')).toBeFalsy(); + }); + + it('should not create project.json file when adding a project if standalone is false', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, false); + + expect(tree.exists('libs/test/project.json')).toBeFalsy(); + }); + + it('should be able to read from standalone projects', () => { + tree.write( + 'workspace.json', + JSON.stringify( + { + projects: { + test: { + root: '/libs/test', + }, + }, + }, + null, + 2 + ) + ); + + const projectConfig = readProjectConfiguration(tree, 'test'); + + expect(projectConfig).toEqual({ + root: '/libs/test', + }); + }); + + it('should update project.json file when updating a project', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, true); + const expectedProjectConfig = { + ...baseTestProjectConfig, + targets: { build: { executor: '' } }, + }; + updateProjectConfiguration(tree, 'test', expectedProjectConfig); + + expect(readJson(tree, 'libs/test/project.json')).toEqual( + expectedProjectConfig + ); + }); + + it('should update workspace.json file when updating an inline project', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, false); + const expectedProjectConfig = { + ...baseTestProjectConfig, + targets: { build: { executor: '' } }, + }; + updateProjectConfiguration(tree, 'test', expectedProjectConfig); + + expect(readJson(tree, 'workspace.json').projects.test).toEqual( + expectedProjectConfig + ); + }); + + it('should remove project.json file when removing project configuration', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, true); + removeProjectConfiguration(tree, 'test'); + + expect(tree.exists('test/project.json')).toBeFalsy(); + }); + + it('should remove project configuration', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, false); + removeProjectConfiguration(tree, 'test'); + + expect(readJson(tree, 'workspace.json').projects.test).toBeUndefined(); + }); + + it('should support workspaces with standalone and inline projects', () => { + addProjectConfiguration(tree, 'test', baseTestProjectConfig, true); + addProjectConfiguration(tree, 'test2', baseTestProjectConfig, false); + + const configurations = getProjects(tree); + + expect(configurations.get('test')).toEqual(baseTestProjectConfig); + expect(configurations.get('test2')).toEqual(baseTestProjectConfig); + }); + }); }); diff --git a/packages/devkit/src/generators/project-configuration.ts b/packages/devkit/src/generators/project-configuration.ts index b9704d33608b9f..ab38a4a56a370a 100644 --- a/packages/devkit/src/generators/project-configuration.ts +++ b/packages/devkit/src/generators/project-configuration.ts @@ -1,26 +1,28 @@ -import type { Tree } from '@nrwl/tao/src/shared/tree'; import { ProjectConfiguration, RawWorkspaceJsonConfiguration, toNewFormat, WorkspaceJsonConfiguration, } from '@nrwl/tao/src/shared/workspace'; -import { readJson, updateJson, writeJson } from '../utils/json'; -import type { - NxJsonConfiguration, - NxJsonProjectConfiguration, -} from '@nrwl/tao/src/shared/nx'; + import { getWorkspaceLayout, getWorkspacePath, } from '../utils/get-workspace-layout'; +import { readJson, updateJson, writeJson } from '../utils/json'; import { joinPathFragments } from '../utils/path'; +import type { Tree } from '@nrwl/tao/src/shared/tree'; +import type { + NxJsonConfiguration, + NxJsonProjectConfiguration, +} from '@nrwl/tao/src/shared/nx'; + export type WorkspaceConfiguration = Omit< WorkspaceJsonConfiguration, 'projects' > & - Omit; + Partial>; /** * Adds project configuration to the Nx workspace. @@ -89,13 +91,13 @@ export function getProjects( tree: Tree ): Map { const workspace = readWorkspace(tree); - const nxJson = readJson(tree, 'nx.json'); + const nxJson = readNxJson(tree); return new Map( Object.keys(workspace.projects || {}).map((projectName) => { return [ projectName, - getProjectConfiguration(tree, projectName, workspace, nxJson), + getProjectConfiguration(projectName, workspace, nxJson), ]; }) ); @@ -109,11 +111,15 @@ export function getProjects( export function readWorkspaceConfiguration(tree: Tree): WorkspaceConfiguration { const workspace = readWorkspace(tree); delete workspace.projects; - const nxJson = readJson(tree, 'nx.json'); - delete nxJson.projects; + + const nxJson = readNxJson(tree); + if (nxJson !== null) { + delete nxJson.projects; + } + return { ...workspace, - ...nxJson, + ...(nxJson === null ? {} : nxJson), }; } @@ -165,9 +171,12 @@ export function updateWorkspaceConfiguration( return { ...json, ...workspace }; } ); - updateJson(tree, 'nx.json', (json) => { - return { ...json, ...nxJson }; - }); + + if (tree.exists('nx.json')) { + updateJson(tree, 'nx.json', (json) => { + return { ...json, ...nxJson }; + }); + } } /** @@ -193,16 +202,16 @@ export function readProjectConfiguration( ); } - const nxJson = readJson(tree, 'nx.json'); + const nxJson = readNxJson(tree); - // TODO: Remove after confirming that nx.json should be optional. - // if (!nxJson.projects[projectName]) { - // throw new Error( - // `Cannot find configuration for '${projectName}' in nx.json` - // ); - // } + return getProjectConfiguration(projectName, workspace, nxJson); +} - return getProjectConfiguration(tree, projectName, workspace, nxJson); +export function readNxJson(tree: Tree): NxJsonConfiguration | null { + if (!tree.exists('nx.json')) { + return null; + } + return readJson(tree, 'nx.json'); } /** @@ -222,24 +231,21 @@ export function isStandaloneProject(tree: Tree, project: string): boolean { } function getProjectConfiguration( - tree: Tree, projectName: string, workspace: WorkspaceJsonConfiguration, - nxJson: NxJsonConfiguration + nxJson: NxJsonConfiguration | null ): ProjectConfiguration & NxJsonProjectConfiguration { return { - ...readWorkspaceSection(tree, workspace, projectName), - ...readNxJsonSection(nxJson, projectName), + ...readWorkspaceSection(workspace, projectName), + ...(nxJson === null ? {} : readNxJsonSection(nxJson, projectName)), }; } function readWorkspaceSection( - tree: Tree, workspace: WorkspaceJsonConfiguration, projectName: string ) { - const config = workspace.projects[projectName]; - return config; + return workspace.projects[projectName]; } function readNxJsonSection(nxJson: NxJsonConfiguration, projectName: string) { @@ -252,9 +258,13 @@ function setProjectConfiguration( projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration, mode: 'create' | 'update' | 'delete', standalone: boolean = false -) { +): void { + const hasNxJson = tree.exists('nx.json'); + if (mode === 'delete') { - addProjectToNxJson(tree, projectName, undefined, mode); + if (hasNxJson) { + addProjectToNxJson(tree, projectName, undefined, mode); + } addProjectToWorkspaceJson(tree, projectName, undefined, mode); return; } @@ -265,7 +275,6 @@ function setProjectConfiguration( ); } - const { tags, implicitDependencies } = projectConfiguration; addProjectToWorkspaceJson( tree, projectName, @@ -273,15 +282,19 @@ function setProjectConfiguration( mode, standalone ); - addProjectToNxJson( - tree, - projectName, - { - tags, - implicitDependencies, - }, - mode - ); + + if (hasNxJson) { + const { tags, implicitDependencies } = projectConfiguration; + addProjectToNxJson( + tree, + projectName, + { + tags, + implicitDependencies, + }, + mode + ); + } } function addProjectToWorkspaceJson( diff --git a/packages/devkit/src/utils/get-workspace-layout.ts b/packages/devkit/src/utils/get-workspace-layout.ts index 2c745b2241fe53..87663d873fb1d5 100644 --- a/packages/devkit/src/utils/get-workspace-layout.ts +++ b/packages/devkit/src/utils/get-workspace-layout.ts @@ -1,10 +1,9 @@ -import type { Tree } from '@nrwl/tao/src/shared/tree'; +import { RawWorkspaceJsonConfiguration } from '@nrwl/tao/src/shared/workspace'; + +import { readNxJson } from '../generators/project-configuration'; import { readJson } from './json'; -import type { NxJsonConfiguration } from '@nrwl/tao/src/shared/nx'; -import { - RawWorkspaceJsonConfiguration, - workspaceConfigName, -} from '@nrwl/tao/src/shared/workspace'; + +import type { Tree } from '@nrwl/tao/src/shared/tree'; /** * Returns workspace defaults. It includes defaults folders for apps and libs, @@ -23,16 +22,16 @@ export function getWorkspaceLayout(tree: Tree): { standaloneAsDefault: boolean; npmScope: string; } { - const nxJson = readJson(tree, 'nx.json'); + const nxJson = readNxJson(tree); const rawWorkspace = readJson( tree, getWorkspacePath(tree) ); return { - appsDir: nxJson.workspaceLayout?.appsDir ?? 'apps', - libsDir: nxJson.workspaceLayout?.libsDir ?? 'libs', - npmScope: nxJson.npmScope, + appsDir: nxJson?.workspaceLayout?.appsDir ?? 'apps', + libsDir: nxJson?.workspaceLayout?.libsDir ?? 'libs', + npmScope: nxJson?.npmScope ?? '', standaloneAsDefault: Object.values(rawWorkspace.projects).reduce( // default for second, third... projects should be based on all projects being defined as a path // for configuration read from ng schematics, this is determined by configFilePath's presence