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

This commit is contained in:
Leosvel Pérez Espinosa 2023-08-17 16:27:47 +01:00 committed by GitHub
parent b9ca7ce09f
commit 47f8b7a8be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 325 additions and 246 deletions

View File

@ -1,6 +1,6 @@
{
"name": "application",
"factory": "./src/generators/application/application",
"factory": "./src/generators/application/application#applicationGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "GeneratorNxApp",
@ -14,13 +14,18 @@
"type": "string",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the application?",
"pattern": "^[a-zA-Z].*$"
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
@ -178,7 +183,7 @@
"aliases": ["app"],
"x-type": "application",
"description": "Creates an Angular application.",
"implementation": "/packages/angular/src/generators/application/application.ts",
"implementation": "/packages/angular/src/generators/application/application#applicationGeneratorInternal.ts",
"hidden": false,
"path": "/packages/angular/src/generators/application/schema.json",
"type": "generator"

View File

@ -1,6 +1,6 @@
{
"name": "host",
"factory": "./src/generators/host/host",
"factory": "./src/generators/host/host#hostInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxMFHost",
@ -19,7 +19,7 @@
"type": "string",
"description": "The name to give to the host Angular application.",
"$default": { "$source": "argv", "index": 0 },
"pattern": "^[a-zA-Z].*$"
"pattern": "^[a-zA-Z][^:]*$"
},
"remotes": {
"type": "array",
@ -35,6 +35,11 @@
"description": "The directory of the new application.",
"type": "string"
},
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
@ -173,7 +178,7 @@
},
"x-type": "application",
"description": "Generate a Host Angular Module Federation Application.",
"implementation": "/packages/angular/src/generators/host/host.ts",
"implementation": "/packages/angular/src/generators/host/host#hostInternal.ts",
"aliases": [],
"hidden": false,
"path": "/packages/angular/src/generators/host/schema.json",

View File

@ -1,6 +1,6 @@
{
"name": "library",
"factory": "./src/generators/library/library",
"factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "GeneratorAngularLibrary",
@ -14,13 +14,18 @@
"description": "The name of the library.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the library?",
"pattern": "^[a-zA-Z].*$"
"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 library is placed.",
"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"]
},
"publishable": {
"type": "boolean",
"default": false,
@ -205,7 +210,7 @@
"aliases": ["lib"],
"x-type": "library",
"description": "Creates an Angular library.",
"implementation": "/packages/angular/src/generators/library/library.ts",
"implementation": "/packages/angular/src/generators/library/library#libraryGeneratorInternal.ts",
"hidden": false,
"path": "/packages/angular/src/generators/library/schema.json",
"type": "generator"

View File

@ -1,6 +1,6 @@
{
"name": "remote",
"factory": "./src/generators/remote/remote",
"factory": "./src/generators/remote/remote#remoteInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxMFRemote",
@ -19,7 +19,7 @@
"type": "string",
"description": "The name to give to the remote Angular app.",
"$default": { "$source": "argv", "index": 0 },
"pattern": "^[a-zA-Z].*$"
"pattern": "^[a-zA-Z][^:]*$"
},
"host": {
"type": "string",
@ -35,6 +35,11 @@
"description": "The directory of the new application.",
"type": "string"
},
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
@ -166,7 +171,7 @@
},
"x-type": "application",
"description": "Generate a Remote Angular Module Federation Application.",
"implementation": "/packages/angular/src/generators/remote/remote.ts",
"implementation": "/packages/angular/src/generators/remote/remote#remoteInternal.ts",
"aliases": [],
"hidden": false,
"path": "/packages/angular/src/generators/remote/schema.json",

View File

@ -1,5 +1,6 @@
import { names } from '@nx/devkit';
import {
checkFilesExist,
cleanupProject,
killProcessAndPorts,
newProject,
@ -195,4 +196,39 @@ describe('Angular Module Federation', () => {
// port and process cleanup
await killProcessAndPorts(process.pid, hostPort, remote1Port, remote2Port);
}, 20_000_000);
it('should should support generating host and remote apps with the new name and root format', async () => {
const hostApp = uniq('host');
const remoteApp = uniq('remote');
const hostPort = 4800;
const remotePort = 4801;
// generate host app
runCLI(
`generate @nx/angular:host ${hostApp} --project-name-and-root-format=as-provided --no-interactive`
);
// generate remote app
runCLI(
`generate @nx/angular:remote ${remoteApp} --host=${hostApp} --port=${remotePort} --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(`${hostApp}/src/app/app.module.ts`);
checkFilesExist(`${remoteApp}/src/app/app.module.ts`);
// check default generated host is built successfully
const buildOutput = runCLI(`build ${hostApp}`);
expect(buildOutput).toContain('Successfully ran target build');
const process = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
(output) =>
output.includes(`listening on localhost:${remotePort}`) &&
output.includes(`listening on localhost:${hostPort}`)
);
// port and process cleanup
await killProcessAndPorts(process.pid, hostPort, remotePort);
}, 20_000_000);
});

View File

@ -350,4 +350,53 @@ describe('Angular Projects', () => {
);
expect(buildOutput).toContain('Successfully ran target build');
});
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/angular: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/app/app.module.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/angular:lib ${libName} --buildable --project-name-and-root-format=derived`
)
).toThrow();
runCLI(
`generate @nx/angular:lib ${libName} --buildable --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(
`${libName}/src/index.ts`,
`${libName}/src/lib/${libName.split('/')[1]}.module.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);
});

View File

@ -163,7 +163,7 @@
"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",
@ -211,7 +211,7 @@
"hidden": true
},
"library": {
"factory": "./src/generators/library/library",
"factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"x-type": "library",
@ -224,7 +224,7 @@
"description": "Creates a secondary entry point for an Angular publishable library."
},
"remote": {
"factory": "./src/generators/remote/remote",
"factory": "./src/generators/remote/remote#remoteInternal",
"schema": "./src/generators/remote/schema.json",
"x-type": "application",
"description": "Generate a Remote Angular Module Federation Application."
@ -241,7 +241,7 @@
"description": "Converts an old micro frontend configuration to use the new withModuleFederation helper. It will run successfully if the following conditions are met: \n - Is either a host or remote application \n - Shared npm package configurations have not been modified \n - Name used to identify the Micro Frontend application matches the project name \n\n{% callout type=\"warning\" title=\"Overrides\" %}This generator will overwrite your webpack config. If you have additional custom configuration in your config file, it will be lost!{% /callout %}"
},
"host": {
"factory": "./src/generators/host/host",
"factory": "./src/generators/host/host#hostInternal",
"schema": "./src/generators/host/schema.json",
"x-type": "application",
"description": "Generate a Host Angular Module Federation Application."

View File

@ -30,6 +30,16 @@ import { prompt } from 'enquirer';
export async function applicationGenerator(
tree: Tree,
schema: Partial<Schema>
): Promise<GeneratorCallback> {
return await applicationGeneratorInternal(tree, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function applicationGeneratorInternal(
tree: Tree,
schema: Partial<Schema>
): Promise<GeneratorCallback> {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
@ -50,7 +60,7 @@ export async function applicationGenerator(
}).then((a) => a['standalone-components']);
}
const options = normalizeOptions(tree, schema);
const options = await normalizeOptions(tree, schema);
const rootOffset = offsetFromRoot(options.appProjectRoot);
await angularInitGenerator(tree, {

View File

@ -10,19 +10,18 @@ import {
} from '@nx/devkit';
import { nxVersion } from '../../../utils/versions';
import type { NormalizedSchema } from './normalized-schema';
import { removeScaffoldedE2e } from './remove-scaffolded-e2e';
import { cypressProjectGenerator } from '@nx/cypress';
export async function addE2e(tree: Tree, options: NormalizedSchema) {
removeScaffoldedE2e(tree, options, options.ngCliSchematicE2ERoot);
if (options.e2eTestRunner === 'cypress') {
// TODO: This can call `@nx/web:static-config` generator when ready
addFileServerTarget(tree, options, 'serve-static');
await cypressProjectGenerator(tree, {
name: options.e2eProjectName,
directory: options.directory,
directory: options.e2eProjectRoot,
// the name and root are already normalized, instruct the generator to use them as is
projectNameAndRootFormat: 'as-provided',
project: options.name,
linter: options.linter,
standaloneConfig: options.standaloneConfig,

View File

@ -7,7 +7,5 @@ export * from './create-project';
export * from './enable-strict-type-checking';
export * from './normalize-options';
export * from './normalized-schema';
export * from './remove-scaffolded-e2e';
export * from './set-app-strict-default';
export * from './update-e2e-project';
export * from './update-editor-tsconfig';

View File

@ -1,45 +1,43 @@
import {
extractLayoutDirectory,
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
import { Linter } from '@nx/linter';
import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
import { normalizeNewProjectPrefix } from '../../utils/project';
import type { Schema } from '../schema';
import type { NormalizedSchema } from './normalized-schema';
import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
import { Linter } from '@nx/linter';
import {
normalizeDirectory,
normalizeNewProjectPrefix,
normalizeProjectName,
} from '../../utils/project';
export function normalizeOptions(
export async function normalizeOptions(
host: Tree,
options: Partial<Schema>
): NormalizedSchema {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.directory
);
const appDirectory = normalizeDirectory(options.name, projectDirectory);
const appProjectName = normalizeProjectName(options.name, projectDirectory);
const e2eProjectName = options.rootProject
? 'e2e'
: `${names(options.name).fileName}-e2e`;
): 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,
});
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
const { appsDir: defaultAppsDir, standaloneAsDefault } =
getWorkspaceLayout(host);
const appsDir = layoutDirectory ?? defaultAppsDir;
const appProjectRoot = options.rootProject
? '.'
: joinPathFragments(appsDir, appDirectory);
const e2eProjectRoot = options.rootProject
? 'e2e'
: joinPathFragments(appsDir, `${appDirectory}-e2e`);
let e2eProjectName = 'e2e';
let e2eProjectRoot = 'e2e';
if (!options.rootProject) {
const projectNameAndRoot = await determineProjectNameAndRootOptions(host, {
name: `${options.name}-e2e`,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
});
e2eProjectName = projectNameAndRoot.projectName;
e2eProjectRoot = projectNameAndRoot.projectRoot;
}
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
@ -51,11 +49,6 @@ export function normalizeOptions(
'app'
);
options.standaloneConfig = options.standaloneConfig ?? standaloneAsDefault;
const ngCliSchematicAppRoot = appProjectName;
const ngCliSchematicE2ERoot = `${appProjectName}/e2e`;
// Set defaults and then overwrite with user options
return {
style: 'css',
@ -76,7 +69,5 @@ export function normalizeOptions(
e2eProjectRoot,
e2eProjectName,
parsedTags,
ngCliSchematicAppRoot,
ngCliSchematicE2ERoot,
};
}

View File

@ -11,6 +11,4 @@ export interface NormalizedSchema extends Schema {
e2eProjectName: string;
e2eProjectRoot: string;
parsedTags: string[];
ngCliSchematicAppRoot: string;
ngCliSchematicE2ERoot: string;
}

View File

@ -1,31 +0,0 @@
import type { Tree } from '@nx/devkit';
import type { NormalizedSchema } from './normalized-schema';
import {
updateProjectConfiguration,
readProjectConfiguration,
} from '@nx/devkit';
export function removeScaffoldedE2e(
host: Tree,
{ name }: NormalizedSchema,
e2eProjectRoot: string
) {
if (host.exists(`${e2eProjectRoot}/src/app.e2e-spec.ts`)) {
host.delete(`${e2eProjectRoot}/src/app.e2e-spec.ts`);
}
if (host.exists(`${e2eProjectRoot}/src/app.po.ts`)) {
host.delete(`${e2eProjectRoot}/src/app.po.ts`);
}
if (host.exists(`${e2eProjectRoot}/protractor.conf.js`)) {
host.delete(`${e2eProjectRoot}/protractor.conf.js`);
}
if (host.exists(`${e2eProjectRoot}/tsconfig.json`)) {
host.delete(`${e2eProjectRoot}/tsconfig.json`);
}
const project = readProjectConfiguration(host, name);
delete project.targets['e2e'];
updateProjectConfiguration(host, name, project);
}

View File

@ -1,66 +0,0 @@
import type { ProjectConfiguration, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
offsetFromRoot,
readProjectConfiguration,
updateJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { getRelativePathToRootTsConfig } from '@nx/js';
import type { NormalizedSchema } from './normalized-schema';
export function updateE2eProject(tree: Tree, options: NormalizedSchema) {
const spec = `${options.e2eProjectRoot}/src/app.e2e-spec.ts`;
const content = tree.read(spec, 'utf-8');
tree.write(
spec,
content.replace(
`${options.name} app is running!`,
`Welcome ${options.name}`
)
);
const page = `${options.e2eProjectRoot}/src/app.po.ts`;
const pageContent = tree.read(page, 'utf-8');
tree.write(page, pageContent.replace(`.content span`, `header h1`));
const proj = readProjectConfiguration(tree, options.name);
const project: ProjectConfiguration = {
root: options.e2eProjectRoot,
projectType: 'application',
targets: {
e2e: proj.targets.e2e,
},
implicitDependencies: [options.name],
tags: [],
};
project.targets.e2e.options.protractorConfig = `${options.e2eProjectRoot}/protractor.conf.js`;
addProjectConfiguration(tree, options.e2eProjectName, project);
delete proj.targets.e2e;
updateProjectConfiguration(tree, options.name, proj);
// update tsconfig e2e
if (!tree.exists(`${options.e2eProjectRoot}/tsconfig.e2e.json`)) {
tree.write(`${options.e2eProjectRoot}/tsconfig.e2e.json`, '{}');
}
updateJson(tree, `${options.e2eProjectRoot}/tsconfig.e2e.json`, (json) => {
return {
...json,
extends: `./tsconfig.json`,
compilerOptions: {
...json.compilerOptions,
outDir: `${offsetFromRoot(options.e2eProjectRoot)}dist/out-tsc`,
},
};
});
// update tsconfig
updateJson(tree, `${options.e2eProjectRoot}/tsconfig.json`, (json) => {
return {
...json,
extends: getRelativePathToRootTsConfig(tree, options.e2eProjectRoot),
};
});
}

View File

@ -1,5 +1,6 @@
import { Linter } from '@nx/linter';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { Styles } from '../utils/types';
export interface Schema {
@ -14,6 +15,7 @@ export interface Schema {
style?: Styles;
skipTests?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
tags?: string;
linter?: Linter;
unitTestRunner?: UnitTestRunner;

View File

@ -14,13 +14,18 @@
"index": 0
},
"x-prompt": "What name would you like to use for the application?",
"pattern": "^[a-zA-Z].*$"
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",

View File

@ -1,23 +1,28 @@
import {
extractLayoutDirectory,
formatFiles,
getProjects,
runTasksInSerial,
stripIndents,
Tree,
} from '@nx/devkit';
import type { Schema } from './schema';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { lt } from 'semver';
import { E2eTestRunner } from '../../utils/test-runners';
import applicationGenerator from '../application/application';
import remoteGenerator from '../remote/remote';
import { normalizeProjectName } from '../utils/project';
import { setupMf } from '../setup-mf/setup-mf';
import { E2eTestRunner } from '../../utils/test-runners';
import { addSsr } from './lib';
import { getInstalledAngularVersionInfo } from '../utils/version-utils';
import { lt } from 'semver';
import { addSsr } from './lib';
import type { Schema } from './schema';
export async function host(tree: Tree, options: Schema) {
return await hostInternal(tree, {
projectNameAndRootFormat: 'derived',
...options,
});
}
export async function hostInternal(tree: Tree, options: Schema) {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) {
@ -40,8 +45,14 @@ export async function host(tree: Tree, options: Schema) {
});
}
const { projectDirectory } = extractLayoutDirectory(options.directory);
const appName = normalizeProjectName(options.name, projectDirectory);
const { projectName: hostProjectName, projectNameAndRootFormat } =
await determineProjectNameAndRootOptions(tree, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
});
options.projectNameAndRootFormat = projectNameAndRootFormat;
const appInstallTask = await applicationGenerator(tree, {
...options,
@ -54,7 +65,7 @@ export async function host(tree: Tree, options: Schema) {
const skipE2E =
!options.e2eTestRunner || options.e2eTestRunner === E2eTestRunner.None;
await setupMf(tree, {
appName,
appName: hostProjectName,
mfType: 'host',
routing: true,
port: 4200,
@ -63,13 +74,13 @@ export async function host(tree: Tree, options: Schema) {
skipPackageJson: options.skipPackageJson,
skipFormat: true,
skipE2E,
e2eProjectName: skipE2E ? undefined : `${appName}-e2e`,
e2eProjectName: skipE2E ? undefined : `${hostProjectName}-e2e`,
prefix: options.prefix,
});
let installTasks = [appInstallTask];
if (options.ssr) {
let ssrInstallTask = await addSsr(tree, options, appName);
let ssrInstallTask = await addSsr(tree, options, hostProjectName);
installTasks.push(ssrInstallTask);
}
@ -77,7 +88,7 @@ export async function host(tree: Tree, options: Schema) {
await remoteGenerator(tree, {
...options,
name: remote,
host: appName,
host: hostProjectName,
skipFormat: true,
standalone: options.standalone,
});

View File

@ -1,5 +1,6 @@
import { Linter } from '@nx/linter';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { Styles } from '../utils/types';
export interface Schema {
@ -14,6 +15,7 @@ export interface Schema {
style?: Styles;
skipTests?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
tags?: string;
linter?: Linter;
unitTestRunner?: UnitTestRunner;

View File

@ -19,7 +19,7 @@
"$source": "argv",
"index": 0
},
"pattern": "^[a-zA-Z].*$"
"pattern": "^[a-zA-Z][^:]*$"
},
"remotes": {
"type": "array",
@ -35,6 +35,11 @@
"description": "The directory of the new application.",
"type": "string"
},
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",

View File

@ -1,22 +1,16 @@
import {
extractLayoutDirectory,
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { names, Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
import { Linter } from '@nx/linter';
import { Schema } from '../schema';
import { NormalizedSchema } from './normalized-schema';
import { UnitTestRunner } from '../../../utils/test-runners';
import { normalizeNewProjectPrefix } from '../../utils/project';
import { Schema } from '../schema';
import { NormalizedSchema } from './normalized-schema';
export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema {
export async function normalizeOptions(
host: Tree,
schema: Schema
): Promise<NormalizedSchema> {
// Create a schema with populated default values
const options: Schema = {
buildable: false,
@ -33,37 +27,32 @@ export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema {
...schema,
};
const name = names(options.name).fileName;
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.directory
);
const fullProjectDirectory = projectDirectory
? `${names(projectDirectory).fileName}/${name}`.replace(/\/+/g, '/')
: name;
const {
projectName,
names: projectNames,
projectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name,
projectType: 'library',
directory: options.directory,
importPath: options.importPath,
projectNameAndRootFormat: options.projectNameAndRootFormat,
});
const { libsDir: defaultLibsDirectory, standaloneAsDefault } =
getWorkspaceLayout(host);
const npmScope = getNpmScope(host);
const libsDir = layoutDirectory ?? defaultLibsDirectory;
const projectName = fullProjectDirectory
.replace(new RegExp('/', 'g'), '-')
.replace(/-\d+/g, '');
const fileName = options.simpleName ? name : projectName;
const projectRoot = joinPathFragments(libsDir, fullProjectDirectory);
const fileName = options.simpleName
? projectNames.projectSimpleName
: projectNames.projectFileName;
const moduleName = `${names(fileName).className}Module`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`;
const npmScope = getNpmScope(host);
const prefix = normalizeNewProjectPrefix(options.prefix, npmScope, 'lib');
options.standaloneConfig = options.standaloneConfig ?? standaloneAsDefault;
const importPath =
options.importPath || getImportPath(host, fullProjectDirectory);
const ngCliSchematicLibRoot = projectName;
const allNormalizedOptions = {
...options,
@ -74,13 +63,14 @@ export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema {
projectRoot,
entryFile: 'index',
moduleName,
projectDirectory: fullProjectDirectory,
modulePath,
parsedTags,
fileName,
importPath,
ngCliSchematicLibRoot,
standaloneComponentName: `${names(name).className}Component`,
standaloneComponentName: `${
names(projectNames.projectSimpleName).className
}Component`,
};
const {

View File

@ -35,7 +35,6 @@ export interface NormalizedSchema {
entryFile: string;
modulePath: string;
moduleName: string;
projectDirectory: string;
parsedTags: string[];
ngCliSchematicLibRoot: string;
standaloneComponentName: string;

View File

@ -571,7 +571,7 @@ describe('lib', () => {
it('should accept numbers in the path', async () => {
await runLibraryGeneratorWithOpts({ directory: 'src/1-api' });
expect(readProjectConfiguration(tree, 'src-api-my-lib').root).toEqual(
expect(readProjectConfiguration(tree, 'src-1-api-my-lib').root).toEqual(
'src/1-api/my-lib'
);
});

View File

@ -36,6 +36,18 @@ import { addProject } from './lib/add-project';
export async function libraryGenerator(
tree: Tree,
schema: Schema
): Promise<GeneratorCallback> {
return await libraryGeneratorInternal(tree, {
// provide a default projectNameAndRootFormat to avoid breaking changes
// to external generators invoking this one
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function libraryGeneratorInternal(
tree: Tree,
schema: Schema
): Promise<GeneratorCallback> {
// Do some validation checks
if (!schema.routing && schema.lazy) {
@ -61,7 +73,7 @@ export async function libraryGenerator(
);
}
const options = normalizeOptions(tree, schema);
const options = await normalizeOptions(tree, schema);
const { libraryOptions } = options;
const pkgVersions = versions(tree);

View File

@ -1,5 +1,6 @@
import { UnitTestRunner } from '../../utils/test-runners';
import { Linter } from '@nx/linter';
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils';
import type { Linter } from '@nx/linter';
import type { UnitTestRunner } from '../../utils/test-runners';
export interface Schema {
name: string;
@ -8,6 +9,7 @@ export interface Schema {
simpleName?: boolean;
addModuleSpec?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
sourceDir?: string;
buildable?: boolean;
publishable?: boolean;

View File

@ -14,13 +14,18 @@
"index": 0
},
"x-prompt": "What name would you like to use for the library?",
"pattern": "^[a-zA-Z].*$"
"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 library is placed.",
"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"]
},
"publishable": {
"type": "boolean",
"default": false,

View File

@ -1,22 +1,27 @@
import {
extractLayoutDirectory,
formatFiles,
getProjects,
runTasksInSerial,
stripIndents,
Tree,
} from '@nx/devkit';
import type { Schema } from './schema';
import applicationGenerator from '../application/application';
import { normalizeProjectName } from '../utils/project';
import { setupMf } from '../setup-mf/setup-mf';
import { E2eTestRunner } from '../../utils/test-runners';
import { addSsr, findNextAvailablePort } from './lib';
import { getInstalledAngularVersionInfo } from '../utils/version-utils';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { lt } from 'semver';
import { E2eTestRunner } from '../../utils/test-runners';
import { applicationGenerator } from '../application/application';
import { setupMf } from '../setup-mf/setup-mf';
import { getInstalledAngularVersionInfo } from '../utils/version-utils';
import { addSsr, findNextAvailablePort } from './lib';
import type { Schema } from './schema';
export async function remote(tree: Tree, options: Schema) {
return await remoteInternal(tree, {
projectNameAndRootFormat: 'derived',
...options,
});
}
export async function remoteInternal(tree: Tree, options: Schema) {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) {
@ -31,8 +36,15 @@ export async function remote(tree: Tree, options: Schema) {
);
}
const { projectDirectory } = extractLayoutDirectory(options.directory);
const appName = normalizeProjectName(options.name, projectDirectory);
const { projectName: remoteProjectName, projectNameAndRootFormat } =
await determineProjectNameAndRootOptions(tree, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
});
options.projectNameAndRootFormat = projectNameAndRootFormat;
const port = options.port ?? findNextAvailablePort(tree);
const appInstallTask = await applicationGenerator(tree, {
@ -47,7 +59,7 @@ export async function remote(tree: Tree, options: Schema) {
!options.e2eTestRunner || options.e2eTestRunner === E2eTestRunner.None;
await setupMf(tree, {
appName,
appName: remoteProjectName,
mfType: 'remote',
routing: true,
host: options.host,
@ -55,7 +67,7 @@ export async function remote(tree: Tree, options: Schema) {
skipPackageJson: options.skipPackageJson,
skipFormat: true,
skipE2E,
e2eProjectName: skipE2E ? undefined : `${appName}-e2e`,
e2eProjectName: skipE2E ? undefined : `${remoteProjectName}-e2e`,
standalone: options.standalone,
prefix: options.prefix,
});
@ -63,7 +75,7 @@ export async function remote(tree: Tree, options: Schema) {
let installTasks = [appInstallTask];
if (options.ssr) {
let ssrInstallTask = await addSsr(tree, {
appName,
appName: remoteProjectName,
port,
standalone: options.standalone,
});

View File

@ -1,5 +1,6 @@
import { Linter } from '@nx/linter';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { Styles } from '../utils/types';
export interface Schema {
@ -13,6 +14,7 @@ export interface Schema {
style?: Styles;
skipTests?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
tags?: string;
linter?: Linter;
unitTestRunner?: UnitTestRunner;

View File

@ -19,7 +19,7 @@
"$source": "argv",
"index": 0
},
"pattern": "^[a-zA-Z].*$"
"pattern": "^[a-zA-Z][^:]*$"
},
"host": {
"type": "string",
@ -35,6 +35,11 @@
"description": "The directory of the new application.",
"type": "string"
},
"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"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",

View File

@ -50,6 +50,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@proj/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -69,6 +70,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -89,6 +91,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@custom-scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -111,6 +114,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: '@scope/lib-name',
projectNameAndRootFormat: 'as-provided',
});
});
@ -135,6 +139,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: 'lib-name',
projectRoot: '.',
projectNameAndRootFormat: 'as-provided',
});
});
@ -181,6 +186,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@proj/shared/lib-name',
projectRoot: 'shared/lib-name',
projectNameAndRootFormat: 'derived',
});
});
@ -215,6 +221,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: 'lib-name',
projectRoot: '.',
projectNameAndRootFormat: 'derived',
});
});
@ -288,6 +295,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
// restore original interactive mode
@ -317,6 +325,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@proj/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -336,6 +345,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -356,6 +366,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@custom-scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
});
@ -379,6 +390,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: '@scope/lib-name',
projectNameAndRootFormat: 'as-provided',
});
});
@ -403,6 +415,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: 'lib-name',
projectRoot: '.',
projectNameAndRootFormat: 'as-provided',
});
});
@ -449,6 +462,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@proj/shared/lib-name',
projectRoot: 'libs/shared/lib-name',
projectNameAndRootFormat: 'derived',
});
});
@ -484,6 +498,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: 'lib-name',
projectRoot: '.',
projectNameAndRootFormat: 'derived',
});
});
@ -557,6 +572,7 @@ describe('determineProjectNameAndRootOptions', () => {
},
importPath: '@scope/lib-name',
projectRoot: 'shared',
projectNameAndRootFormat: 'as-provided',
});
// restore original interactive mode

View File

@ -57,13 +57,20 @@ type ProjectNameAndRootFormats = {
export async function determineProjectNameAndRootOptions(
tree: Tree,
options: ProjectGenerationOptions
): Promise<ProjectNameAndRootOptions> {
): Promise<
ProjectNameAndRootOptions & {
projectNameAndRootFormat: ProjectNameAndRootFormat;
}
> {
validateName(options.name, options.projectNameAndRootFormat);
const formats = getProjectNameAndRootFormats(tree, options);
const format =
options.projectNameAndRootFormat ?? (await determineFormat(formats));
return formats[format];
return {
...formats[format],
projectNameAndRootFormat: format,
};
}
function validateName(