Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): use helper to determine project name and root directory in project generators #18620

Merged
12 changes: 9 additions & 3 deletions docs/generated/packages/node/generators/application.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "application",
"factory": "./src/generators/application/application",
"factory": "./src/generators/application/application#applicationGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
Expand All @@ -14,13 +14,19 @@
"type": "string",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the node application?",
"x-priority": "important"
"x-priority": "important",
"pattern": "^[a-zA-Z][^:]*$"
},
"directory": {
"description": "The directory of the new application.",
"type": "string",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
Expand Down Expand Up @@ -131,7 +137,7 @@
"aliases": ["app"],
"x-type": "application",
"description": "Create a node application.",
"implementation": "/packages/node/src/generators/application/application.ts",
"implementation": "/packages/node/src/generators/application/application#applicationGeneratorInternal.ts",
"hidden": false,
"path": "/packages/node/src/generators/application/schema.json",
"type": "generator"
Expand Down
12 changes: 9 additions & 3 deletions docs/generated/packages/node/generators/library.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "library",
"factory": "./src/generators/library/library",
"factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
Expand All @@ -19,13 +19,19 @@
"type": "string",
"description": "Library name",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the library?"
"x-prompt": "What name would you like to use for the library?",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
},
"directory": {
"type": "string",
"description": "A directory where the lib is placed",
"alias": "dir"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"simpleModuleName": {
"description": "Keep the module name simple (when using `--directory`).",
"type": "boolean",
Expand Down Expand Up @@ -131,7 +137,7 @@
"aliases": ["lib"],
"x-type": "library",
"description": "Create a node library.",
"implementation": "/packages/node/src/generators/library/library.ts",
"implementation": "/packages/node/src/generators/library/library#libraryGeneratorInternal.ts",
"hidden": false,
"path": "/packages/node/src/generators/library/schema.json",
"type": "generator"
Expand Down
46 changes: 46 additions & 0 deletions e2e/node/src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,52 @@ ${jslib}();
checkFilesExist(`dist/apps/_should_keep.txt`);
}, 120000);

it('should support generating projects with the new name and root format', () => {
const appName = uniq('app1');
const libName = uniq('@my-org/lib1');

runCLI(
`generate @nx/node:app ${appName} --project-name-and-root-format=as-provided --no-interactive`
);

// check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${appName}/src/main.ts`);
// check build works
expect(runCLI(`build ${appName}`)).toContain(
`Successfully ran target build for project ${appName}`
);
// check tests pass
const appTestResult = runCLI(`test ${appName}`);
expect(appTestResult).toContain(
`Successfully ran target test for project ${appName}`
);

// assert scoped project names are not supported when --project-name-and-root-format=derived
expect(() =>
runCLI(
`generate @nx/node:lib ${libName} --buildable --project-name-and-root-format=derived --no-interactive`
)
).toThrow();

runCLI(
`generate @nx/node:lib ${libName} --buildable --project-name-and-root-format=as-provided --no-interactive`
);

// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${libName}/src/index.ts`);
// check build works
expect(runCLI(`build ${libName}`)).toContain(
`Successfully ran target build for project ${libName}`
);
// check tests pass
const libTestResult = runCLI(`test ${libName}`);
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
}, 500_000);

describe('NestJS', () => {
it('should have plugin output if specified in `tsPlugins`', async () => {
newProject();
Expand Down
4 changes: 2 additions & 2 deletions packages/node/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"hidden": true
},
"application": {
"factory": "./src/generators/application/application",
"factory": "./src/generators/application/application#applicationGeneratorInternal",
"schema": "./src/generators/application/schema.json",
"aliases": ["app"],
"x-type": "application",
"description": "Create a node application."
},
"library": {
"factory": "./src/generators/library/library",
"factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"x-type": "library",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('app', () => {

expect(() =>
readProjectConfiguration(tree, 'my-dir-my-node-app-e2e')
).toThrow(/Cannot find/);
).not.toThrow();
});

it('should update tags', async () => {
Expand Down
60 changes: 34 additions & 26 deletions packages/node/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import {
addProjectConfiguration,
convertNxGenerator,
ensurePackage,
extractLayoutDirectory,
formatFiles,
generateFiles,
GeneratorCallback,
getWorkspaceLayout,
joinPathFragments,
logger,
names,
Expand All @@ -22,13 +20,13 @@ import {
updateProjectConfiguration,
updateTsConfigsToJs,
} from '@nx/devkit';
import { Linter, lintProjectGenerator } from '@nx/linter';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { configurationGenerator } from '@nx/jest';

import { getRelativePathToRootTsConfig, tsConfigBaseOptions } from '@nx/js';
import { esbuildVersion } from '@nx/js/src/utils/versions';
import { Linter, lintProjectGenerator } from '@nx/linter';
import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project';
import { join } from 'path';

import { initGenerator } from '../init/init';
import {
expressTypingsVersion,
expressVersion,
Expand All @@ -41,11 +39,9 @@ import {
nxVersion,
} from '../../utils/versions';
import { e2eProjectGenerator } from '../e2e-project/e2e-project';
import { initGenerator } from '../init/init';
import { setupDockerGenerator } from '../setup-docker/setup-docker';

import { Schema } from './schema';
import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project';
import { esbuildVersion } from '@nx/js/src/utils/versions';

export interface NormalizedSchema extends Schema {
appProjectRoot: string;
Expand Down Expand Up @@ -364,7 +360,14 @@ function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) {
}

export async function applicationGenerator(tree: Tree, schema: Schema) {
const options = normalizeOptions(tree, schema);
return await applicationGeneratorInternal(tree, {
projectNameAndRootFormat: 'derived',
...schema,
});
}

export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
const options = await normalizeOptions(tree, schema);
const tasks: GeneratorCallback[] = [];

if (options.framework === 'nest') {
Expand Down Expand Up @@ -414,6 +417,8 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
...options,
projectType: options.framework === 'none' ? 'cli' : 'server',
name: options.rootProject ? 'e2e' : `${options.name}-e2e`,
directory: options.rootProject ? 'e2e' : `${options.appProjectRoot}-e2e`,
projectNameAndRootFormat: 'as-provided',
project: options.name,
port: options.port,
isNest: options.isNest,
Expand Down Expand Up @@ -447,21 +452,24 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
return runTasksInSerial(...tasks);
}

function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.directory
);
const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir;

const appDirectory = projectDirectory
? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
: names(options.name).fileName;

const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');

const appProjectRoot = options.rootProject
? '.'
: joinPathFragments(appsDir, appDirectory);
async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
const {
projectName: appProjectName,
projectRoot: appProjectRoot,
projectNameAndRootFormat,
} = await determineProjectNameAndRootOptions(host, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator: '@nx/node:application',
});
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;

options.bundler = options.bundler ?? 'esbuild';
options.e2eTestRunner = options.e2eTestRunner ?? 'jest';
Expand All @@ -472,7 +480,7 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {

return {
...options,
name: names(appProjectName).fileName,
name: appProjectName,
frontendProject: options.frontendProject
? names(options.frontendProject).fileName
: undefined,
Expand Down
4 changes: 3 additions & 1 deletion packages/node/src/generators/application/schema.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Linter } from '@nx/linter';
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';

export interface Schema {
name: string;
skipFormat?: boolean;
skipPackageJson?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
unitTestRunner?: 'jest' | 'none';
e2eTestRunner?: 'jest' | 'none';
linter?: Linter;
Expand Down
8 changes: 7 additions & 1 deletion packages/node/src/generators/application/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@
"index": 0
},
"x-prompt": "What name would you like to use for the node application?",
"x-priority": "important"
"x-priority": "important",
"pattern": "^[a-zA-Z][^:]*$"
},
"directory": {
"description": "The directory of the new application.",
"type": "string",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
Expand Down
58 changes: 32 additions & 26 deletions packages/node/src/generators/e2e-project/e2e-project.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import * as path from 'path';
import {
addDependenciesToPackageJson,
addProjectConfiguration,
convertNxGenerator,
extractLayoutDirectory,
formatFiles,
generateFiles,
GeneratorCallback,
getWorkspaceLayout,
joinPathFragments,
names,
offsetFromRoot,
Expand All @@ -16,19 +13,30 @@ import {
Tree,
updateJson,
} from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter, lintProjectGenerator } from '@nx/linter';

import { Schema } from './schema';
import { axiosVersion } from '../../utils/versions';
import { join } from 'path';
import {
globalJavaScriptOverrides,
globalTypeScriptOverrides,
} from '@nx/linter/src/generators/init/global-eslint-config';
import * as path from 'path';
import { join } from 'path';
import { axiosVersion } from '../../utils/versions';
import { Schema } from './schema';

export async function e2eProjectGenerator(host: Tree, _options: Schema) {
export async function e2eProjectGenerator(host: Tree, options: Schema) {
return await e2eProjectGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...options,
});
}

export async function e2eProjectGeneratorInternal(
host: Tree,
_options: Schema
) {
const tasks: GeneratorCallback[] = [];
const options = normalizeOptions(host, _options);
const options = await normalizeOptions(host, _options);
const appProject = readProjectConfiguration(host, options.project);

addProjectConfiguration(host, options.e2eProjectName, {
Expand Down Expand Up @@ -146,25 +154,23 @@ export async function e2eProjectGenerator(host: Tree, _options: Schema) {
return runTasksInSerial(...tasks);
}

function normalizeOptions(
async function normalizeOptions(
tree: Tree,
options: Schema
): Omit<Schema, 'name'> & { e2eProjectRoot: string; e2eProjectName: string } {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.directory
);
const appsDir = layoutDirectory ?? getWorkspaceLayout(tree).appsDir;
const name = options.name ?? `${options.project}-e2e`;

const appDirectory = projectDirectory
? `${names(projectDirectory).fileName}/${names(name).fileName}`
: names(name).fileName;

const e2eProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');

const e2eProjectRoot = options.rootProject
? 'e2e'
: joinPathFragments(appsDir, appDirectory);
): Promise<
Omit<Schema, 'name'> & { e2eProjectRoot: string; e2eProjectName: string }
> {
const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } =
await determineProjectNameAndRootOptions(tree, {
name: options.name ?? `${options.project}-e2e`,
projectType: 'library',
directory: options.rootProject ? 'e2e' : options.directory,
projectNameAndRootFormat: options.rootProject
? 'as-provided'
: options.projectNameAndRootFormat,
// this is an internal generator, don't save defaults
callingGenerator: null,
});

return {
...options,
Expand Down
Loading