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', () => { let tree: Tree; let originalInteractiveValue; let originalCIValue; let originalIsTTYValue; function ensureInteractiveMode() { process.env.NX_INTERACTIVE = 'true'; process.env.CI = 'false'; process.stdout.isTTY = true; } function restoreOriginalInteractiveMode() { process.env.NX_INTERACTIVE = originalInteractiveValue; process.env.CI = originalCIValue; process.stdout.isTTY = originalIsTTYValue; } beforeEach(() => { originalInteractiveValue = process.env.NX_INTERACTIVE; originalCIValue = process.env.CI; originalIsTTYValue = process.stdout.isTTY; }); describe('no layout', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace(); jest.clearAllMocks(); }); it('should return the project name and directory as provided', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toStrictEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@proj/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it(`should handle window's style paths correctly when format is "as-provided"`, async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared\\libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toStrictEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@proj/lib-name', projectRoot: 'shared/lib-name', projectNameAndRootFormat: 'as-provided', }); }); it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it('should use provided import path over scoped name when format is "as-provided"', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', importPath: '@custom-scope/lib-name', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@custom-scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it('should return the directory as the project name when directory is not provided and format is "as-provided"', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: '@scope/lib-name', projectNameAndRootFormat: 'as-provided', }); }); it('should return the project name and directory as provided for root projects', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result).toEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: 'lib-name', projectRoot: '.', projectNameAndRootFormat: 'as-provided', }); }); it('should derive import path for root projects when package.json does not have a name and format is as-provided', async () => { updateJson(tree, 'package.json', (json) => { json.name = undefined; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); }); it('should throw when an invalid name is provided', async () => { await expect( determineProjectNameAndRootOptions(tree, { name: '!scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }) ).rejects.toThrowError(); }); it('should return the derived project name and directory', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }); expect(result).toEqual({ projectName: 'shared-lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'shared-lib-name', }, importPath: '@proj/shared/lib-name', projectRoot: 'shared/lib-name', projectNameAndRootFormat: 'derived', }); }); it('should throw when using a scoped package name as the project name and format is "derived"', async () => { await expect( determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }) ).rejects.toThrowError(); }); it('should return the derived project name and directory for root projects', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'derived', rootProject: true, callingGenerator: '', }); expect(result).toEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: 'lib-name', projectRoot: '.', projectNameAndRootFormat: 'derived', }); }); it('should derive import path for root projects when package.json does not have a name and format is "derived"', async () => { updateJson(tree, 'package.json', (json) => { json.name = undefined; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); }); it(`should handle window's style paths correctly when format is "derived"`, async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared\\sub-dir', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }); expect(result).toStrictEqual({ projectName: 'shared-sub-dir-lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'shared-sub-dir-lib-name', }, importPath: '@proj/shared/sub-dir/lib-name', projectRoot: 'shared/sub-dir/lib-name', projectNameAndRootFormat: 'derived', }); }); it('should prompt for the project name and root format', async () => { // simulate interactive mode ensureInteractiveMode(); const promptSpy = jest .spyOn(enquirer, 'prompt') .mockImplementation(() => Promise.resolve({ format: 'as-provided' })); await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', directory: 'shared', callingGenerator: '', }); expect(promptSpy).toHaveBeenCalled(); const promptCallOptions = promptSpy.mock.calls[0][0] as any; expect(promptCallOptions.choices).toStrictEqual([ { message: `As provided: Name: lib-name Root: shared`, name: 'lib-name @ shared', }, { message: `Derived: Name: shared-lib-name Root: shared/lib-name`, name: 'shared-lib-name @ shared/lib-name', }, ]); // restore original interactive mode 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(); const promptSpy = jest.spyOn(enquirer, 'prompt'); const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', projectType: 'library', directory: 'shared', callingGenerator: '', }); expect(promptSpy).not.toHaveBeenCalled(); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); // restore original interactive mode restoreOriginalInteractiveMode(); }); }); describe('with layout', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); jest.clearAllMocks(); }); it('should return the project name and directory as provided', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@proj/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it(`should handle window's style paths correctly when format is "as-provided"`, async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared\\libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toStrictEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@proj/lib-name', projectRoot: 'shared/lib-name', projectNameAndRootFormat: 'as-provided', }); }); it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it('should use provided import path over scoped name when format is "as-provided"', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', importPath: '@custom-scope/lib-name', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@custom-scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); }); it('should return the directory as the project name when directory is not provided and format is "as-provided"', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: '@scope/lib-name', projectNameAndRootFormat: 'as-provided', }); }); it('should return the project name and directory as provided for root projects', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result).toEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: 'lib-name', projectRoot: '.', projectNameAndRootFormat: 'as-provided', }); }); it('should derive import path for root projects when package.json does not have a name and format is "as-provided"', async () => { updateJson(tree, 'package.json', (json) => { json.name = undefined; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); }); it('should throw when an invalid name is provided', async () => { await expect( determineProjectNameAndRootOptions(tree, { name: '!scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'as-provided', callingGenerator: '', }) ).rejects.toThrowError(); }); it('should return the derived project name and directory', async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }); expect(result).toEqual({ projectName: 'shared-lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'shared-lib-name', }, importPath: '@proj/shared/lib-name', projectRoot: 'libs/shared/lib-name', projectNameAndRootFormat: 'derived', }); }); it(`should handle window's style paths correctly when format is "derived"`, async () => { const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', directory: 'shared\\sub-dir', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }); expect(result).toStrictEqual({ projectName: 'shared-sub-dir-lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'shared-sub-dir-lib-name', }, importPath: '@proj/shared/sub-dir/lib-name', projectRoot: 'libs/shared/sub-dir/lib-name', projectNameAndRootFormat: 'derived', }); }); it('should throw when using a scoped package name as the project name and format is derived', async () => { await expect( determineProjectNameAndRootOptions(tree, { name: '@scope/libName', directory: 'shared', projectType: 'library', projectNameAndRootFormat: 'derived', callingGenerator: '', }) ).rejects.toThrowError(); }); it('should return the derived project name and directory for root projects', async () => { updateJson(tree, 'package.json', (json) => { json.name = 'lib-name'; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'derived', rootProject: true, callingGenerator: '', }); expect(result).toEqual({ projectName: 'lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: 'lib-name', projectRoot: '.', projectNameAndRootFormat: 'derived', }); }); it('should derive import path for root projects when package.json does not have a name and format is "derived"', async () => { updateJson(tree, 'package.json', (json) => { json.name = undefined; return json; }); const result = await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', projectNameAndRootFormat: 'as-provided', rootProject: true, callingGenerator: '', }); expect(result.importPath).toBe('@proj/lib-name'); }); it('should prompt for the project name and root format', async () => { // simulate interactive mode ensureInteractiveMode(); const promptSpy = jest .spyOn(enquirer, 'prompt') .mockImplementation(() => Promise.resolve({ format: 'as-provided' })); await determineProjectNameAndRootOptions(tree, { name: 'libName', projectType: 'library', directory: 'shared', callingGenerator: '', }); expect(promptSpy).toHaveBeenCalled(); const promptCallOptions = promptSpy.mock.calls[0][0] as any; expect(promptCallOptions.choices).toStrictEqual([ { message: `As provided: Name: lib-name Root: shared`, name: 'lib-name @ shared', }, { message: `Derived: Name: shared-lib-name Root: libs/shared/lib-name`, name: 'shared-lib-name @ libs/shared/lib-name', }, ]); // 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(); const promptSpy = jest.spyOn(enquirer, 'prompt'); const result = await determineProjectNameAndRootOptions(tree, { name: '@scope/libName', projectType: 'library', directory: 'shared', callingGenerator: '', }); expect(promptSpy).not.toHaveBeenCalled(); expect(result).toEqual({ projectName: '@scope/lib-name', names: { projectSimpleName: 'lib-name', projectFileName: 'lib-name', }, importPath: '@scope/lib-name', projectRoot: 'shared', projectNameAndRootFormat: 'as-provided', }); // restore original interactive mode restoreOriginalInteractiveMode(); }); }); });