feat(nx-plugin): use helper to determine project name and root in project generators (#18739)

This commit is contained in:
Leosvel Pérez Espinosa 2023-08-22 17:28:23 +01:00 committed by GitHub
parent 1b0439b55c
commit 016c89fed6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 210 additions and 89 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "create-package", "name": "create-package",
"factory": "./src/generators/create-package/create-package", "factory": "./src/generators/create-package/create-package#createPackageGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx", "cli": "nx",
@ -35,6 +35,11 @@
"type": "string", "type": "string",
"description": "A directory where the app is placed." "description": "A directory where the app is placed."
}, },
"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"]
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",
@ -70,7 +75,7 @@
"presets": [] "presets": []
}, },
"description": "Create a package which can be used by npx to create a new workspace", "description": "Create a package which can be used by npx to create a new workspace",
"implementation": "/packages/plugin/src/generators/create-package/create-package.ts", "implementation": "/packages/plugin/src/generators/create-package/create-package#createPackageGeneratorInternal.ts",
"aliases": [], "aliases": [],
"hidden": false, "hidden": false,
"path": "/packages/plugin/src/generators/create-package/schema.json", "path": "/packages/plugin/src/generators/create-package/schema.json",

View File

@ -1,6 +1,6 @@
{ {
"name": "e2e-project", "name": "e2e-project",
"factory": "./src/generators/e2e-project/e2e", "factory": "./src/generators/e2e-project/e2e#e2eProjectGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx", "cli": "nx",
@ -24,6 +24,11 @@
"type": "string", "type": "string",
"description": "the directory where the plugin is placed." "description": "the directory where the plugin is placed."
}, },
"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"]
},
"pluginOutputPath": { "pluginOutputPath": {
"type": "string", "type": "string",
"description": "the output path of the plugin after it builds.", "description": "the output path of the plugin after it builds.",
@ -52,7 +57,7 @@
"presets": [] "presets": []
}, },
"description": "Create a E2E application for a Nx Plugin.", "description": "Create a E2E application for a Nx Plugin.",
"implementation": "/packages/plugin/src/generators/e2e-project/e2e.ts", "implementation": "/packages/plugin/src/generators/e2e-project/e2e#e2eProjectGeneratorInternal.ts",
"aliases": [], "aliases": [],
"hidden": false, "hidden": false,
"path": "/packages/plugin/src/generators/e2e-project/schema.json", "path": "/packages/plugin/src/generators/e2e-project/schema.json",

View File

@ -1,6 +1,6 @@
{ {
"name": "plugin", "name": "plugin",
"factory": "./src/generators/plugin/plugin", "factory": "./src/generators/plugin/plugin#pluginGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx", "cli": "nx",
@ -20,12 +20,18 @@
"description": "Plugin name", "description": "Plugin name",
"$default": { "$source": "argv", "index": 0 }, "$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the plugin?", "x-prompt": "What name would you like to use for the plugin?",
"x-priority": "important" "x-priority": "important",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
}, },
"directory": { "directory": {
"type": "string", "type": "string",
"description": "A directory where the plugin is placed." "description": "A directory where the plugin is placed."
}, },
"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"]
},
"importPath": { "importPath": {
"type": "string", "type": "string",
"description": "How the plugin will be published, like `@myorg/my-awesome-plugin`. Note this must be a valid NPM name.", "description": "How the plugin will be published, like `@myorg/my-awesome-plugin`. Note this must be a valid NPM name.",
@ -98,7 +104,7 @@
"presets": [] "presets": []
}, },
"description": "Create a Nx Plugin.", "description": "Create a Nx Plugin.",
"implementation": "/packages/plugin/src/generators/plugin/plugin.ts", "implementation": "/packages/plugin/src/generators/plugin/plugin#pluginGeneratorInternal.ts",
"aliases": [], "aliases": [],
"hidden": false, "hidden": false,
"path": "/packages/plugin/src/generators/plugin/schema.json", "path": "/packages/plugin/src/generators/plugin/schema.json",

View File

@ -434,4 +434,43 @@ describe('Nx Plugin', () => {
) )
).toThrow(); ).toThrow();
}); });
it('should support the new name and root format', async () => {
const plugin = uniq('plugin');
const createAppName = `create-${plugin}-app`;
runCLI(
`generate @nx/plugin:plugin ${plugin} --e2eTestRunner jest --publishable --project-name-and-root-format=as-provided`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${plugin}/src/index.ts`);
// check build works
expect(runCLI(`build ${plugin}`)).toContain(
`Successfully ran target build for project ${plugin}`
);
// check tests pass
const appTestResult = runCLI(`test ${plugin}`);
expect(appTestResult).toContain(
`Successfully ran target test for project ${plugin}`
);
runCLI(
`generate @nx/plugin:create-package ${createAppName} --project=${plugin} --e2eProject=${plugin}-e2e --project-name-and-root-format=as-provided`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${plugin}/src/generators/preset`, `${createAppName}`);
// check build works
expect(runCLI(`build ${createAppName}`)).toContain(
`Successfully ran target build for project ${createAppName}`
);
// check tests pass
const libTestResult = runCLI(`test ${createAppName}`);
expect(libTestResult).toContain(
`Successfully ran target test for project ${createAppName}`
);
});
}); });

View File

@ -4,17 +4,17 @@
"extends": ["@nx/workspace"], "extends": ["@nx/workspace"],
"generators": { "generators": {
"plugin": { "plugin": {
"factory": "./src/generators/plugin/plugin", "factory": "./src/generators/plugin/plugin#pluginGeneratorInternal",
"schema": "./src/generators/plugin/schema.json", "schema": "./src/generators/plugin/schema.json",
"description": "Create a Nx Plugin." "description": "Create a Nx Plugin."
}, },
"create-package": { "create-package": {
"factory": "./src/generators/create-package/create-package", "factory": "./src/generators/create-package/create-package#createPackageGeneratorInternal",
"schema": "./src/generators/create-package/schema.json", "schema": "./src/generators/create-package/schema.json",
"description": "Create a package which can be used by npx to create a new workspace" "description": "Create a package which can be used by npx to create a new workspace"
}, },
"e2e-project": { "e2e-project": {
"factory": "./src/generators/e2e-project/e2e", "factory": "./src/generators/e2e-project/e2e#e2eProjectGeneratorInternal",
"schema": "./src/generators/e2e-project/schema.json", "schema": "./src/generators/e2e-project/schema.json",
"description": "Create a E2E application for a Nx Plugin." "description": "Create a E2E application for a Nx Plugin."
}, },

View File

@ -26,10 +26,20 @@ import { join } from 'path';
export async function createPackageGenerator( export async function createPackageGenerator(
host: Tree, host: Tree,
schema: CreatePackageSchema schema: CreatePackageSchema
) {
return await createPackageGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function createPackageGeneratorInternal(
host: Tree,
schema: CreatePackageSchema
) { ) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = normalizeSchema(host, schema); const options = await normalizeSchema(host, schema);
const pluginPackageName = await addPresetGenerator(host, options); const pluginPackageName = await addPresetGenerator(host, options);
const installTask = addDependenciesToPackageJson( const installTask = addDependenciesToPackageJson(

View File

@ -1,3 +1,4 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils';
import type { Linter } from '@nx/linter'; import type { Linter } from '@nx/linter';
export interface CreatePackageSchema { export interface CreatePackageSchema {
@ -6,6 +7,7 @@ export interface CreatePackageSchema {
// options to create cli package, passed to js library generator // options to create cli package, passed to js library generator
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
skipFormat: boolean; skipFormat: boolean;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'none';

View File

@ -37,6 +37,11 @@
"type": "string", "type": "string",
"description": "A directory where the app is placed." "description": "A directory where the app is placed."
}, },
"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"]
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",

View File

@ -1,42 +1,36 @@
import { import { Tree } from '@nx/devkit';
extractLayoutDirectory, import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { CreatePackageSchema } from '../schema'; import { CreatePackageSchema } from '../schema';
export interface NormalizedSchema extends CreatePackageSchema { export interface NormalizedSchema extends CreatePackageSchema {
bundler: 'swc' | 'tsc'; bundler: 'swc' | 'tsc';
libsDir: string;
projectName: string; projectName: string;
projectRoot: string; projectRoot: string;
projectDirectory: string;
} }
export function normalizeSchema( export async function normalizeSchema(
host: Tree, host: Tree,
schema: CreatePackageSchema schema: CreatePackageSchema
): NormalizedSchema { ): Promise<NormalizedSchema> {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory( const {
schema.directory projectName,
); names: projectNames,
const { libsDir: defaultLibsDir } = getWorkspaceLayout(host); projectRoot,
const libsDir = layoutDirectory ?? defaultLibsDir; projectNameAndRootFormat,
const name = names(schema.name).fileName; } = await determineProjectNameAndRootOptions(host, {
const fullProjectDirectory = projectDirectory name: schema.name,
? `${names(projectDirectory).fileName}/${name}` projectType: 'library',
: name; directory: schema.directory,
const projectName = fullProjectDirectory.replace(new RegExp('/', 'g'), '-'); projectNameAndRootFormat: schema.projectNameAndRootFormat,
const projectRoot = joinPathFragments(libsDir, fullProjectDirectory); callingGenerator: '@nx/plugin:create-package',
});
schema.projectNameAndRootFormat = projectNameAndRootFormat;
return { return {
...schema, ...schema,
bundler: schema.compiler ?? 'tsc', bundler: schema.compiler ?? 'tsc',
libsDir,
projectName, projectName,
projectRoot, projectRoot,
name, name: projectNames.projectSimpleName,
projectDirectory: fullProjectDirectory,
}; };
} }

View File

@ -16,13 +16,13 @@ import {
runTasksInSerial, runTasksInSerial,
updateProjectConfiguration, updateProjectConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { addPropertyToJestConfig, configurationGenerator } from '@nx/jest'; import { addPropertyToJestConfig, configurationGenerator } from '@nx/jest';
import { getRelativePathToRootTsConfig } from '@nx/js'; import { getRelativePathToRootTsConfig } from '@nx/js';
import { setupVerdaccio } from '@nx/js/src/generators/setup-verdaccio/generator'; import { setupVerdaccio } from '@nx/js/src/generators/setup-verdaccio/generator';
import { addLocalRegistryScripts } from '@nx/js/src/utils/add-local-registry-scripts'; import { addLocalRegistryScripts } from '@nx/js/src/utils/add-local-registry-scripts';
import { join } from 'path';
import { Linter, lintProjectGenerator } from '@nx/linter'; import { Linter, lintProjectGenerator } from '@nx/linter';
import { join } from 'path';
import type { Schema } from './schema'; import type { Schema } from './schema';
interface NormalizedSchema extends Schema { interface NormalizedSchema extends Schema {
@ -32,20 +32,42 @@ interface NormalizedSchema extends Schema {
linter: Linter; linter: Linter;
} }
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
const projectName = options.rootProject ? 'e2e' : `${options.pluginName}-e2e`;
let projectRoot: string;
if (options.projectNameAndRootFormat === 'as-provided') {
const projectNameAndRootOptions = await determineProjectNameAndRootOptions(
host,
{
name: projectName,
projectType: 'application',
directory:
options.rootProject || !options.projectDirectory
? projectName
: `${options.projectDirectory}-e2e`,
projectNameAndRootFormat: `as-provided`,
callingGenerator: '@nx/plugin:e2e-project',
}
);
projectRoot = projectNameAndRootOptions.projectRoot;
} else {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory( const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.projectDirectory options.projectDirectory
); );
const { appsDir: defaultAppsDir } = getWorkspaceLayout(host); const { appsDir: defaultAppsDir } = getWorkspaceLayout(host);
const appsDir = layoutDirectory ?? defaultAppsDir; const appsDir = layoutDirectory ?? defaultAppsDir;
const projectName = options.rootProject ? 'e2e' : `${options.pluginName}-e2e`; projectRoot = options.rootProject
const projectRoot =
projectDirectory && !options.rootProject
? joinPathFragments(appsDir, `${projectDirectory}-e2e`)
: options.rootProject
? projectName ? projectName
: projectDirectory
? joinPathFragments(appsDir, `${projectDirectory}-e2e`)
: joinPathFragments(appsDir, projectName); : joinPathFragments(appsDir, projectName);
}
const pluginPropertyName = names(options.pluginName).propertyName; const pluginPropertyName = names(options.pluginName).propertyName;
return { return {
@ -158,10 +180,17 @@ async function addLintingToApplication(
} }
export async function e2eProjectGenerator(host: Tree, schema: Schema) { export async function e2eProjectGenerator(host: Tree, schema: Schema) {
return await e2eProjectGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
validatePlugin(host, schema.pluginName); validatePlugin(host, schema.pluginName);
const options = normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
addFiles(host, options); addFiles(host, options);
tasks.push( tasks.push(
await setupVerdaccio(host, { await setupVerdaccio(host, {

View File

@ -1,9 +1,11 @@
import { Linter } from '@nx/linter'; import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils';
import type { Linter } from '@nx/linter';
export interface Schema { export interface Schema {
pluginName: string; pluginName: string;
npmPackageName: string; npmPackageName: string;
projectDirectory?: string; projectDirectory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
pluginOutputPath?: string; pluginOutputPath?: string;
jestConfig?: string; jestConfig?: string;
linter?: Linter; linter?: Linter;

View File

@ -21,6 +21,11 @@
"type": "string", "type": "string",
"description": "the directory where the plugin is placed." "description": "the directory where the plugin is placed."
}, },
"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"]
},
"pluginOutputPath": { "pluginOutputPath": {
"type": "string", "type": "string",
"description": "the output path of the plugin after it builds.", "description": "the output path of the plugin after it builds.",

View File

@ -4,6 +4,7 @@ import {
formatFiles, formatFiles,
generateFiles, generateFiles,
GeneratorCallback, GeneratorCallback,
joinPathFragments,
normalizePath, normalizePath,
readProjectConfiguration, readProjectConfiguration,
runTasksInSerial, runTasksInSerial,
@ -74,7 +75,14 @@ function updatePluginConfig(host: Tree, options: NormalizedSchema) {
} }
export async function pluginGenerator(host: Tree, schema: Schema) { export async function pluginGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema); return await pluginGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function pluginGeneratorInternal(host: Tree, schema: Schema) {
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
tasks.push( tasks.push(
@ -119,10 +127,14 @@ export async function pluginGenerator(host: Tree, schema: Schema) {
await e2eProjectGenerator(host, { await e2eProjectGenerator(host, {
pluginName: options.name, pluginName: options.name,
projectDirectory: options.projectDirectory, projectDirectory: options.projectDirectory,
pluginOutputPath: `dist/${options.libsDir}/${options.projectDirectory}`, pluginOutputPath: joinPathFragments(
'dist',
options.rootProject ? options.name : options.projectRoot
),
npmPackageName: options.npmPackageName, npmPackageName: options.npmPackageName,
skipFormat: true, skipFormat: true,
rootProject: options.rootProject, rootProject: options.rootProject,
projectNameAndRootFormat: options.projectNameAndRootFormat,
}) })
); );
} }

View File

@ -1,8 +1,10 @@
import { Linter } from '@nx/linter'; import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils';
import type { Linter } from '@nx/linter';
export interface Schema { export interface Schema {
name: string; name: string;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
importPath?: string; importPath?: string;
skipTsConfig?: boolean; // default is false skipTsConfig?: boolean; // default is false
skipFormat?: boolean; // default is false skipFormat?: boolean; // default is false

View File

@ -20,12 +20,18 @@
"index": 0 "index": 0
}, },
"x-prompt": "What name would you like to use for the plugin?", "x-prompt": "What name would you like to use for the plugin?",
"x-priority": "important" "x-priority": "important",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
}, },
"directory": { "directory": {
"type": "string", "type": "string",
"description": "A directory where the plugin is placed." "description": "A directory where the plugin is placed."
}, },
"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"]
},
"importPath": { "importPath": {
"type": "string", "type": "string",
"description": "How the plugin will be published, like `@myorg/my-awesome-plugin`. Note this must be a valid NPM name.", "description": "How the plugin will be published, like `@myorg/my-awesome-plugin`. Note this must be a valid NPM name.",

View File

@ -1,17 +1,10 @@
import { import { Tree, extractLayoutDirectory, getWorkspaceLayout } from '@nx/devkit';
extractLayoutDirectory, import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
libsDir: string;
projectRoot: string; projectRoot: string;
projectDirectory: string; projectDirectory: string;
parsedTags: string[]; parsedTags: string[];
@ -19,44 +12,50 @@ export interface NormalizedSchema extends Schema {
bundler: 'swc' | 'tsc'; bundler: 'swc' | 'tsc';
publishable: boolean; publishable: boolean;
} }
export function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): NormalizedSchema { ): Promise<NormalizedSchema> {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory( const {
options.directory projectName,
); projectRoot,
const { libsDir: defaultLibsDir } = getWorkspaceLayout(host); importPath: npmPackageName,
const libsDir = layoutDirectory ?? defaultLibsDir; projectNameAndRootFormat,
const name = names(options.name).fileName; } = await determineProjectNameAndRootOptions(host, {
const fullProjectDirectory = projectDirectory name: options.name,
? `${names(projectDirectory).fileName}/${name}` projectType: 'library',
: options.rootProject directory: options.directory,
? '.' importPath: options.importPath,
: name; projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator: '@nx/plugin:plugin',
});
options.projectNameAndRootFormat = projectNameAndRootFormat;
options.rootProject = projectRoot === '.';
const projectName = options.rootProject let projectDirectory = projectRoot;
? name if (options.projectNameAndRootFormat === 'derived') {
: fullProjectDirectory.replace(new RegExp('/', 'g'), '-'); let { layoutDirectory } = extractLayoutDirectory(options.directory);
const fileName = projectName; if (!layoutDirectory) {
const projectRoot = options.rootProject const { libsDir } = getWorkspaceLayout(host);
? fullProjectDirectory layoutDirectory = libsDir;
: joinPathFragments(libsDir, fullProjectDirectory); }
if (projectRoot.startsWith(`${layoutDirectory}/`)) {
projectDirectory = projectRoot.replace(`${layoutDirectory}/`, '');
}
}
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const npmPackageName = options.importPath || getImportPath(host, name);
return { return {
...options, ...options,
bundler: options.compiler ?? 'tsc', bundler: options.compiler ?? 'tsc',
fileName, fileName: projectName,
libsDir,
name: projectName, name: projectName,
projectRoot, projectRoot,
projectDirectory: fullProjectDirectory, projectDirectory,
parsedTags, parsedTags,
npmPackageName, npmPackageName,
publishable: options.publishable ?? false, publishable: options.publishable ?? false,