diff --git a/packages/angular/src/generators/application/lib/normalize-options.ts b/packages/angular/src/generators/application/lib/normalize-options.ts index f677f4d57be3c..a71fb5f2b6207 100644 --- a/packages/angular/src/generators/application/lib/normalize-options.ts +++ b/packages/angular/src/generators/application/lib/normalize-options.ts @@ -21,6 +21,7 @@ export async function normalizeOptions( directory: options.directory, projectNameAndRootFormat: options.projectNameAndRootFormat, rootProject: options.rootProject, + callingGenerator: '@nx/angular:application', }); options.rootProject = appProjectRoot === '.'; options.projectNameAndRootFormat = projectNameAndRootFormat; @@ -34,6 +35,7 @@ export async function normalizeOptions( directory: options.directory, projectNameAndRootFormat: options.projectNameAndRootFormat, rootProject: options.rootProject, + callingGenerator: '@nx/angular:application', }); e2eProjectName = projectNameAndRoot.projectName; e2eProjectRoot = projectNameAndRoot.projectRoot; diff --git a/packages/angular/src/generators/host/host.ts b/packages/angular/src/generators/host/host.ts index 13fd02dd27eb7..c1ee2a346fdee 100644 --- a/packages/angular/src/generators/host/host.ts +++ b/packages/angular/src/generators/host/host.ts @@ -51,6 +51,7 @@ export async function hostInternal(tree: Tree, options: Schema) { projectType: 'application', directory: options.directory, projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/angular:host', }); options.projectNameAndRootFormat = projectNameAndRootFormat; diff --git a/packages/angular/src/generators/library/lib/normalize-options.ts b/packages/angular/src/generators/library/lib/normalize-options.ts index 278a01138a6fe..9fb8c6fa82171 100644 --- a/packages/angular/src/generators/library/lib/normalize-options.ts +++ b/packages/angular/src/generators/library/lib/normalize-options.ts @@ -38,6 +38,7 @@ export async function normalizeOptions( directory: options.directory, importPath: options.importPath, projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/angular:library', }); const fileName = options.simpleName diff --git a/packages/angular/src/generators/remote/remote.ts b/packages/angular/src/generators/remote/remote.ts index 7104f49ad8cc3..633a2ed2eff08 100644 --- a/packages/angular/src/generators/remote/remote.ts +++ b/packages/angular/src/generators/remote/remote.ts @@ -42,6 +42,7 @@ export async function remoteInternal(tree: Tree, options: Schema) { projectType: 'application', directory: options.directory, projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/angular:remote', }); options.projectNameAndRootFormat = projectNameAndRootFormat; diff --git a/packages/cypress/src/generators/cypress-project/cypress-project.ts b/packages/cypress/src/generators/cypress-project/cypress-project.ts index 5407c318279ed..634d2880f04f8 100644 --- a/packages/cypress/src/generators/cypress-project/cypress-project.ts +++ b/packages/cypress/src/generators/cypress-project/cypress-project.ts @@ -250,6 +250,7 @@ async function normalizeOptions( projectNameAndRootFormat: isRootProject ? 'as-provided' : options.projectNameAndRootFormat, + callingGenerator: '@nx/cypress:cypress-project', } ); diff --git a/packages/devkit/src/generators/project-name-and-root-utils.spec.ts b/packages/devkit/src/generators/project-name-and-root-utils.spec.ts index 1876b0e2540e2..3d950488df004 100644 --- a/packages/devkit/src/generators/project-name-and-root-utils.spec.ts +++ b/packages/devkit/src/generators/project-name-and-root-utils.spec.ts @@ -2,6 +2,7 @@ import * as enquirer from 'enquirer'; import { createTreeWithEmptyWorkspace } from 'nx/src/generators/testing-utils/create-tree-with-empty-workspace'; import type { Tree } from 'nx/src/generators/tree'; import { updateJson } from 'nx/src/generators/utils/json'; +import { readNxJson } from 'nx/src/generators/utils/nx-json'; import { determineProjectNameAndRootOptions } from './project-name-and-root-utils'; describe('determineProjectNameAndRootOptions', () => { @@ -40,6 +41,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toStrictEqual({ @@ -60,6 +62,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toEqual({ @@ -81,6 +84,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', importPath: '@custom-scope/lib-name', + callingGenerator: '', }); expect(result).toEqual({ @@ -104,6 +108,7 @@ describe('determineProjectNameAndRootOptions', () => { name: '@scope/libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toEqual({ @@ -129,6 +134,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result).toEqual({ @@ -154,6 +160,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); @@ -166,6 +173,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }) ).rejects.toThrowError(); }); @@ -176,6 +184,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', + callingGenerator: '', }); expect(result).toEqual({ @@ -197,6 +206,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', + callingGenerator: '', }) ).rejects.toThrowError(); }); @@ -211,6 +221,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'derived', rootProject: true, + callingGenerator: '', }); expect(result).toEqual({ @@ -236,6 +247,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); @@ -252,6 +264,7 @@ describe('determineProjectNameAndRootOptions', () => { name: 'libName', projectType: 'library', directory: 'shared', + callingGenerator: '', }); expect(promptSpy).toHaveBeenCalled(); @@ -275,6 +288,32 @@ describe('determineProjectNameAndRootOptions', () => { restoreOriginalInteractiveMode(); }); + it('should prompt to save default when as-provided is choosen', async () => { + // simulate interactive mode + ensureInteractiveMode(); + const promptSpy = jest + .spyOn(enquirer, 'prompt') + .mockImplementation(() => + Promise.resolve({ format: 'lib-name @ shared', saveDefault: true }) + ); + + await determineProjectNameAndRootOptions(tree, { + name: 'libName', + projectType: 'library', + directory: 'shared', + callingGenerator: '@nx/some-plugin:app', + }); + + expect(promptSpy).toHaveBeenCalledTimes(2); + + expect(readNxJson(tree).generators['@nx/some-plugin:app']).toEqual({ + projectNameAndRootFormat: 'as-provided', + }); + + // restore original interactive mode + restoreOriginalInteractiveMode(); + }); + it('should directly use format as-provided and not prompt when the name is a scoped package name', async () => { // simulate interactive mode ensureInteractiveMode(); @@ -284,6 +323,7 @@ describe('determineProjectNameAndRootOptions', () => { name: '@scope/libName', projectType: 'library', directory: 'shared', + callingGenerator: '', }); expect(promptSpy).not.toHaveBeenCalled(); @@ -315,6 +355,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toEqual({ @@ -335,6 +376,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toEqual({ @@ -356,6 +398,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', importPath: '@custom-scope/lib-name', + callingGenerator: '', }); expect(result).toEqual({ @@ -380,6 +423,7 @@ describe('determineProjectNameAndRootOptions', () => { name: '@scope/libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }); expect(result).toEqual({ @@ -405,6 +449,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result).toEqual({ @@ -430,6 +475,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); @@ -442,6 +488,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', + callingGenerator: '', }) ).rejects.toThrowError(); }); @@ -452,6 +499,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', + callingGenerator: '', }); expect(result).toEqual({ @@ -473,6 +521,7 @@ describe('determineProjectNameAndRootOptions', () => { directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', + callingGenerator: '', }) ).rejects.toThrowError(); }); @@ -488,6 +537,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'derived', rootProject: true, + callingGenerator: '', }); expect(result).toEqual({ @@ -513,6 +563,7 @@ describe('determineProjectNameAndRootOptions', () => { projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, + callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); @@ -529,6 +580,7 @@ describe('determineProjectNameAndRootOptions', () => { name: 'libName', projectType: 'library', directory: 'shared', + callingGenerator: '', }); expect(promptSpy).toHaveBeenCalled(); @@ -561,6 +613,7 @@ describe('determineProjectNameAndRootOptions', () => { name: '@scope/libName', projectType: 'library', directory: 'shared', + callingGenerator: '', }); expect(promptSpy).not.toHaveBeenCalled(); diff --git a/packages/devkit/src/generators/project-name-and-root-utils.ts b/packages/devkit/src/generators/project-name-and-root-utils.ts index 42133b255b2d9..4f2fddf0f20c7 100644 --- a/packages/devkit/src/generators/project-name-and-root-utils.ts +++ b/packages/devkit/src/generators/project-name-and-root-utils.ts @@ -8,12 +8,13 @@ import { } from '../utils/get-workspace-layout'; import { names } from '../utils/names'; -const { joinPathFragments, readJson, readNxJson } = requireNx(); +const { joinPathFragments, readJson, readNxJson, updateNxJson } = requireNx(); export type ProjectNameAndRootFormat = 'as-provided' | 'derived'; export type ProjectGenerationOptions = { name: string; projectType: ProjectType; + callingGenerator: string; directory?: string; importPath?: string; projectNameAndRootFormat?: ProjectNameAndRootFormat; @@ -65,7 +66,8 @@ export async function determineProjectNameAndRootOptions( validateName(options.name, options.projectNameAndRootFormat); const formats = getProjectNameAndRootFormats(tree, options); const format = - options.projectNameAndRootFormat ?? (await determineFormat(formats)); + options.projectNameAndRootFormat ?? + (await determineFormat(tree, formats, options.callingGenerator)); return { ...formats[format], @@ -104,7 +106,9 @@ function validateName( } async function determineFormat( - formats: ProjectNameAndRootFormats + tree: Tree, + formats: ProjectNameAndRootFormats, + callingGenerator: string ): Promise { if (!formats.derived) { return 'as-provided'; @@ -123,7 +127,7 @@ async function determineFormat( Root: ${formats['derived'].projectRoot}`; const derivedSelectedValue = `${formats['derived'].projectName} @ ${formats['derived'].projectRoot} (This was derived from the folder structure. Please provide the exact name and directory in the future)`; - return await prompt<{ format: ProjectNameAndRootFormat }>({ + const result = await prompt<{ format: ProjectNameAndRootFormat }>({ type: 'select', name: 'format', message: @@ -142,6 +146,22 @@ async function determineFormat( }).then(({ format }) => format === asProvidedSelectedValue ? 'as-provided' : 'derived' ); + if (result === 'as-provided' && callingGenerator) { + const { saveDefault } = await prompt<{ saveDefault: boolean }>({ + type: 'confirm', + message: 'Would you like to save this layout as a default?', + name: 'saveDefault', + }); + if (saveDefault) { + const nxJson = readNxJson(tree); + nxJson.generators ??= {}; + nxJson.generators[callingGenerator] ??= {}; + nxJson.generators[callingGenerator].projectNameAndRootFormat = result; + updateNxJson(tree, nxJson); + } + } + + return result; } function getProjectNameAndRootFormats( diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 7b495ddaf30ff..362e302e672db 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -550,6 +550,7 @@ async function normalizeOptions( importPath: options.importPath, projectNameAndRootFormat: options.projectNameAndRootFormat, rootProject: options.rootProject, + callingGenerator: '@nx/js:library', }); options.rootProject = projectRoot === '.'; const fileName = getCaseAwareFileName({