feat(angular): add directive generator (#15630)

This commit is contained in:
Colum Ferry 2023-03-14 15:31:49 +00:00 committed by GitHub
parent 636a74b62a
commit 481899ce9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 913 additions and 70 deletions

View File

@ -3699,6 +3699,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "directive",
"path": "/packages/angular/generators/directive",
"name": "directive",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "init",
"path": "/packages/angular/generators/init",

View File

@ -189,6 +189,15 @@
"path": "/packages/angular/generators/convert-tslint-to-eslint",
"type": "generator"
},
"/packages/angular/generators/directive": {
"description": "Generate an Angular directive.",
"file": "generated/packages/angular/generators/directive.json",
"hidden": false,
"name": "directive",
"originalFilePath": "/packages/angular/src/generators/directive/schema.json",
"path": "/packages/angular/generators/directive",
"type": "generator"
},
"/packages/angular/generators/init": {
"description": "Initializes the `@nrwl/angular` plugin.",
"file": "generated/packages/angular/generators/init.json",

View File

@ -183,6 +183,15 @@
"path": "angular/generators/convert-tslint-to-eslint",
"type": "generator"
},
{
"description": "Generate an Angular directive.",
"file": "generated/packages/angular/generators/directive.json",
"hidden": false,
"name": "directive",
"originalFilePath": "/packages/angular/src/generators/directive/schema.json",
"path": "angular/generators/directive",
"type": "generator"
},
{
"description": "Initializes the `@nrwl/angular` plugin.",
"file": "generated/packages/angular/generators/init.json",

View File

@ -0,0 +1,90 @@
{
"name": "directive",
"factory": "./src/generators/directive/directive",
"schema": {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "GeneratorAngularDirective",
"cli": "nx",
"title": "Nx Angular Directive Options Schema",
"type": "object",
"description": "Creates a new, generic directive definition in the given project.",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The name of the new directive.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the directive?"
},
"path": {
"type": "string",
"format": "path",
"$default": { "$source": "workingDirectory" },
"description": "The path at which to create the interface that defines the directive, relative to the workspace root.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"x-dropdown": "projects"
},
"prefix": {
"type": "string",
"description": "A prefix to apply to generated selectors.",
"alias": "p",
"oneOf": [
{ "maxLength": 0 },
{ "minLength": 1, "format": "html-selector" }
]
},
"skipTests": {
"type": "boolean",
"description": "Do not create \"spec.ts\" test files for the new class.",
"default": false
},
"skipImport": {
"type": "boolean",
"description": "Do not import this directive into the owning NgModule.",
"default": false
},
"selector": {
"type": "string",
"format": "html-selector",
"description": "The HTML selector to use for this directive."
},
"standalone": {
"description": "Whether the generated directive is standalone.",
"type": "boolean",
"default": false
},
"flat": {
"type": "boolean",
"description": "When true (the default), creates the new files at the top level of the current project.",
"default": true
},
"module": {
"type": "string",
"description": "The filename of the declaring NgModule.",
"alias": "m"
},
"export": {
"type": "boolean",
"default": false,
"description": "The declaring NgModule exports this directive."
},
"skipFormat": {
"type": "boolean",
"default": false,
"description": "Skip formatting of files."
}
},
"required": ["name", "project"],
"presets": []
},
"aliases": ["d"],
"description": "Generate an Angular directive.",
"implementation": "/packages/angular/src/generators/directive/directive.ts",
"hidden": false,
"path": "/packages/angular/src/generators/directive/schema.json",
"type": "generator"
}

View File

@ -341,7 +341,7 @@ describe('create-nx-workspace', () => {
process.env.SELECTED_PM = packageManager;
});
it('should store package manager preference for angular cli', () => {
it('should store package manager preference for angular', () => {
const wsName = uniq('pm');
const appName = uniq('app');

View File

@ -195,6 +195,12 @@
"schema": "./src/generators/convert-tslint-to-eslint/schema.json",
"description": "Converts a project from TSLint to ESLint."
},
"directive": {
"factory": "./src/generators/directive/directive",
"schema": "./src/generators/directive/schema.json",
"aliases": ["d"],
"description": "Generate an Angular directive."
},
"init": {
"factory": "./src/generators/init/init",
"schema": "./src/generators/init/schema.json",

View File

@ -4,6 +4,7 @@ export * from './src/generators/component-cypress-spec/component-cypress-spec';
export * from './src/generators/component-story/component-story';
export * from './src/generators/component/component';
export * from './src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint';
export * from './src/generators/directive/directive';
export * from './src/generators/host/host';
export * from './src/generators/init/init';
export * from './src/generators/library-secondary-entry-point/library-secondary-entry-point';

View File

@ -1,17 +1,11 @@
import type { Tree } from '@nrwl/devkit';
import {
formatFiles,
normalizePath,
readNxJson,
readProjectConfiguration,
stripIndents,
} from '@nrwl/devkit';
import { pathStartsWith } from '../utils/path';
import { formatFiles, stripIndents } from '@nrwl/devkit';
import { exportComponentInEntryPoint } from './lib/component';
import { normalizeOptions } from './lib/normalize-options';
import type { NormalizedSchema, Schema } from './schema';
import type { Schema } from './schema';
import { getInstalledAngularVersionInfo } from '../utils/version-utils';
import { lt } from 'semver';
import { checkPathUnderProjectRoot } from '../utils/path';
export async function componentGenerator(tree: Tree, rawOptions: Schema) {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
@ -27,7 +21,7 @@ export async function componentGenerator(tree: Tree, rawOptions: Schema) {
const options = await normalizeOptions(tree, rawOptions);
const { projectSourceRoot, ...schematicOptions } = options;
checkPathUnderProjectRoot(tree, options);
checkPathUnderProjectRoot(tree, options.project, options.path);
const { wrapAngularDevkitSchematic } = require('@nrwl/devkit/ngcli-adapter');
const angularComponentSchematic = wrapAngularDevkitSchematic(
@ -41,25 +35,4 @@ export async function componentGenerator(tree: Tree, rawOptions: Schema) {
await formatFiles(tree);
}
function checkPathUnderProjectRoot(tree: Tree, schema: NormalizedSchema): void {
if (!schema.path) {
return;
}
const project = schema.project ?? readNxJson(tree).defaultProject;
const { root } = readProjectConfiguration(tree, project);
let pathToComponent = normalizePath(schema.path);
pathToComponent = pathToComponent.startsWith('/')
? pathToComponent.slice(1)
: pathToComponent;
if (!pathStartsWith(pathToComponent, root)) {
throw new Error(
`The path provided for the component (${schema.path}) does not exist under the project root (${root}). ` +
`Please make sure to provide a path that exists under the project root.`
);
}
}
export default componentGenerator;

View File

@ -0,0 +1,247 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`directive generator should export the directive correctly when flat=false and path is nested deeper 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should export the directive correctly when flat=false and path is nested deeper 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should export the directive correctly when flat=false and path is nested deeper 3`] = `
"import {NgModule} from \\"@angular/core\\";
import { TestDirective } from './my-directives/test/test.directive';
@NgModule({
imports: [],
declarations: [, TestDirective],
exports: [, TestDirective]
})
export class TestModule {}"
`;
exports[`directive generator should generate a directive with test files and attach to the NgModule automatically 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should generate a directive with test files and attach to the NgModule automatically 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should generate a directive with test files and attach to the NgModule automatically 3`] = `
"import {NgModule} from \\"@angular/core\\";
import { TestDirective } from './test.directive';
@NgModule({
imports: [],
declarations: [, TestDirective],
exports: []
})
export class TestModule {}"
`;
exports[`directive generator should import the directive correctly when flat=false 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should import the directive correctly when flat=false 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should import the directive correctly when flat=false 3`] = `
"import {NgModule} from \\"@angular/core\\";
import { TestDirective } from './test/test.directive';
@NgModule({
imports: [],
declarations: [, TestDirective],
exports: []
})
export class TestModule {}"
`;
exports[`directive generator should import the directive correctly when flat=false and path is nested deeper 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should import the directive correctly when flat=false and path is nested deeper 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should import the directive correctly when flat=false and path is nested deeper 3`] = `
"import {NgModule} from \\"@angular/core\\";
import { TestDirective } from './my-directives/test/test.directive';
@NgModule({
imports: [],
declarations: [, TestDirective],
exports: []
})
export class TestModule {}"
`;
exports[`directive generator should not generate test file when skipTests=true 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should not generate test file when skipTests=true 2`] = `
"import {NgModule} from \\"@angular/core\\";
import { TestDirective } from './my-directives/test/test.directive';
@NgModule({
imports: [],
declarations: [, TestDirective],
exports: []
})
export class TestModule {}"
`;
exports[`directive generator should not import the directive when skipImport=true 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]'
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should not import the directive when skipImport=true 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should not import the directive when skipImport=true 3`] = `
"import {NgModule} from \\"@angular/core\\";
@NgModule({
imports: [],
declarations: [],
exports: []
})
export class TestModule {}"
`;
exports[`directive generator should not import the directive when standalone=true 1`] = `
"import { Directive } from '@angular/core';
@Directive({
selector: '[projTest]',
standalone: true
})
export class TestDirective {
constructor() { }
}
"
`;
exports[`directive generator should not import the directive when standalone=true 2`] = `
"import { TestDirective } from './test.directive';
describe('TestDirective', () => {
it('should create an instance', () => {
const directive = new TestDirective();
expect(directive).toBeTruthy();
});
});
"
`;
exports[`directive generator should not import the directive when standalone=true 3`] = `
"import {NgModule} from \\"@angular/core\\";
@NgModule({
imports: [],
declarations: [],
exports: []
})
export class TestModule {}"
`;

View File

@ -0,0 +1,163 @@
import { addProjectConfiguration, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { directiveGenerator } from './directive';
import type { Schema } from './schema';
describe('directive generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'test', {
root: 'test',
sourceRoot: 'test/src',
});
tree.write(
'test/src/test.module.ts',
`import {NgModule} from "@angular/core";
@NgModule({
imports: [],
declarations: [],
exports: []
})
export class TestModule {}`
);
});
it('should generate a directive with test files and attach to the NgModule automatically', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree);
// ASSERT
expect(tree.read('test/src/test.directive.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/src/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should import the directive correctly when flat=false', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, { flat: false });
// ASSERT
expect(
tree.read('test/src/test/test.directive.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/src/test/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should not import the directive when standalone=true', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, { standalone: true });
// ASSERT
expect(tree.read('test/src/test.directive.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/src/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should import the directive correctly when flat=false and path is nested deeper', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, {
flat: false,
path: 'test/src/my-directives',
});
// ASSERT
expect(
tree.read('test/src/my-directives/test/test.directive.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/src/my-directives/test/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should export the directive correctly when flat=false and path is nested deeper', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, {
flat: false,
path: 'test/src/my-directives',
export: true,
});
// ASSERT
expect(
tree.read('test/src/my-directives/test/test.directive.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/src/my-directives/test/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should not import the directive when skipImport=true', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, {
flat: false,
path: 'test/src/my-directives',
skipImport: true,
});
// ASSERT
expect(
tree.read('test/src/my-directives/test/test.directive.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/src/my-directives/test/test.directive.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
it('should not generate test file when skipTests=true', async () => {
// ARRANGE
// ACT
await generateDirectiveWithDefaultOptions(tree, {
flat: false,
path: 'test/src/my-directives',
skipTests: true,
});
// ASSERT
expect(
tree.read('test/src/my-directives/test/test.directive.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.exists('test/src/my-directives/test/test.directive.spec.ts')
).toBeFalsy();
expect(tree.read('test/src/test.module.ts', 'utf-8')).toMatchSnapshot();
});
});
async function generateDirectiveWithDefaultOptions(
tree: Tree,
overrides: Partial<Schema> = {}
) {
await directiveGenerator(tree, {
name: 'test',
project: 'test',
flat: true,
...overrides,
});
}

View File

@ -0,0 +1,160 @@
import type { ProjectConfiguration, Tree } from '@nrwl/devkit';
import {
formatFiles,
generateFiles,
getProjects,
joinPathFragments,
names,
readNxJson,
readProjectConfiguration,
} from '@nrwl/devkit';
import type { Schema } from './schema';
import { checkPathUnderProjectRoot } from '../utils/path';
import { dirname } from 'path';
import { insertNgModuleProperty } from '../utils';
import { insertImport } from '@nrwl/js';
import { ensureTypescript } from '@nrwl/js/src/utils/typescript/ensure-typescript';
let tsModule: typeof import('typescript');
export async function directiveGenerator(tree: Tree, schema: Schema) {
const projects = getProjects(tree);
if (!projects.has(schema.project)) {
throw new Error(`Project "${schema.project}" does not exist!`);
}
checkPathUnderProjectRoot(tree, schema.project, schema.path);
const project = readProjectConfiguration(
tree,
schema.project
) as ProjectConfiguration & { prefix?: string };
const path = schema.path ?? `${project.sourceRoot}`;
const directiveNames = names(schema.name);
const selector =
schema.selector ??
buildSelector(tree, schema.name, schema.prefix ?? project.prefix);
const pathToGenerateFiles = schema.flat
? './files/__directiveFileName__'
: './files';
await generateFiles(
tree,
joinPathFragments(__dirname, pathToGenerateFiles),
path,
{
selector,
directiveClassName: directiveNames.className,
directiveFileName: directiveNames.fileName,
standalone: schema.standalone,
tpl: '',
}
);
if (schema.skipTests) {
const pathToSpecFile = joinPathFragments(
path,
`${!schema.flat ? `${directiveNames.fileName}/` : ``}${
directiveNames.fileName
}.directive.spec.ts`
);
tree.delete(pathToSpecFile);
}
if (!schema.skipImport && !schema.standalone) {
const modulePath = findModule(tree, path, schema.module);
addImportToNgModule(path, modulePath, schema, directiveNames, tree);
}
if (!schema.skipFormat) {
await formatFiles(tree);
}
}
function buildSelector(tree: Tree, name: string, prefix: string) {
let selector = names(name).fileName;
const selectorPrefix = names(prefix ?? readNxJson(tree).npmScope).fileName;
return names(`${selectorPrefix}-${selector}`).propertyName;
}
function findModule(tree: Tree, path: string, module?: string) {
let modulePath = '';
let pathToSearch = path;
while (pathToSearch !== '/') {
if (module) {
const pathToModule = joinPathFragments(pathToSearch, module);
if (tree.exists(pathToModule)) {
modulePath = pathToModule;
break;
}
} else {
const potentialOptions = tree
.children(pathToSearch)
.filter((f) => f.endsWith('.module.ts'));
if (potentialOptions.length > 1) {
throw new Error(
`More than one NgModule was found. Please provide the NgModule you wish to use.`
);
} else if (potentialOptions.length === 1) {
modulePath = joinPathFragments(pathToSearch, potentialOptions[0]);
break;
}
}
pathToSearch = dirname(pathToSearch);
}
const moduleContents = tree.read(modulePath, 'utf-8');
if (!moduleContents.includes('@NgModule')) {
throw new Error(
`Declaring module file (${modulePath}) does not contain an @NgModule Declaration.`
);
}
return modulePath;
}
function addImportToNgModule(
path: string,
modulePath: string,
schema: Schema,
directiveNames: {
name: string;
className: string;
propertyName: string;
constantName: string;
fileName: string;
},
tree: Tree
) {
if (!tsModule) {
tsModule = ensureTypescript();
}
let relativePath = `${joinPathFragments(
path.replace(dirname(modulePath), ''),
!schema.flat ? directiveNames.fileName : '',
`${directiveNames.fileName}.directive`
)}`;
relativePath = relativePath.startsWith('/')
? `.${relativePath}`
: `./${relativePath}`;
const directiveClassName = `${directiveNames.className}Directive`;
const moduleContents = tree.read(modulePath, 'utf-8');
const source = tsModule.createSourceFile(
modulePath,
moduleContents,
tsModule.ScriptTarget.Latest,
true
);
insertImport(tree, source, modulePath, directiveClassName, relativePath);
insertNgModuleProperty(tree, modulePath, directiveClassName, 'declarations');
if (schema.export) {
insertNgModuleProperty(tree, modulePath, directiveClassName, 'exports');
}
}
export default directiveGenerator;

View File

@ -0,0 +1,8 @@
import { <%= directiveClassName %>Directive } from './<%= directiveFileName %>.directive';
describe('<%= directiveClassName %>Directive', () => {
it('should create an instance', () => {
const directive = new <%= directiveClassName %>Directive();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { Directive } from '@angular/core';
@Directive({
selector: '[<%= selector %>]'<% if(standalone) {%>,
standalone: true<%}%>
})
export class <%= directiveClassName %>Directive {
constructor() { }
}

View File

@ -0,0 +1,14 @@
export interface Schema {
name: string;
project: string;
path?: string;
prefix?: string;
skipTests?: boolean;
skipImport?: boolean;
selector?: string;
standalone?: boolean;
flat?: boolean;
module?: string;
export?: boolean;
skipFormat?: boolean;
}

View File

@ -0,0 +1,89 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "GeneratorAngularDirective",
"cli": "nx",
"title": "Nx Angular Directive Options Schema",
"type": "object",
"description": "Creates a new, generic directive definition in the given project.",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The name of the new directive.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the directive?"
},
"path": {
"type": "string",
"format": "path",
"$default": {
"$source": "workingDirectory"
},
"description": "The path at which to create the interface that defines the directive, relative to the workspace root.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"x-dropdown": "projects"
},
"prefix": {
"type": "string",
"description": "A prefix to apply to generated selectors.",
"alias": "p",
"oneOf": [
{
"maxLength": 0
},
{
"minLength": 1,
"format": "html-selector"
}
]
},
"skipTests": {
"type": "boolean",
"description": "Do not create \"spec.ts\" test files for the new class.",
"default": false
},
"skipImport": {
"type": "boolean",
"description": "Do not import this directive into the owning NgModule.",
"default": false
},
"selector": {
"type": "string",
"format": "html-selector",
"description": "The HTML selector to use for this directive."
},
"standalone": {
"description": "Whether the generated directive is standalone.",
"type": "boolean",
"default": false
},
"flat": {
"type": "boolean",
"description": "When true (the default), creates the new files at the top level of the current project.",
"default": true
},
"module": {
"type": "string",
"description": "The filename of the declaring NgModule.",
"alias": "m"
},
"export": {
"type": "boolean",
"default": false,
"description": "The declaring NgModule exports this directive."
},
"skipFormat": {
"type": "boolean",
"default": false,
"description": "Skip formatting of files."
}
},
"required": ["name", "project"]
}

View File

@ -1,25 +1,17 @@
import { applyChangesToString, ChangeType, Tree } from '@nrwl/devkit';
import {
import type {
__String,
CallExpression,
ClassDeclaration,
createSourceFile,
Decorator,
getDecorators,
ImportDeclaration,
isArrayLiteralExpression,
isCallExpression,
isClassDeclaration,
isIdentifier,
isImportDeclaration,
isNamedImports,
isObjectLiteralExpression,
isPropertyAssignment,
ObjectLiteralExpression,
PropertyAssignment,
ScriptTarget,
SourceFile,
} from 'typescript';
import { ensureTypescript } from '@nrwl/js/src/utils/typescript/ensure-typescript';
let tsModule: typeof import('typescript');
type ngModuleDecoratorProperty =
| 'imports'
@ -33,12 +25,15 @@ export function insertNgModuleProperty(
name: string,
property: ngModuleDecoratorProperty
) {
if (!tsModule) {
tsModule = ensureTypescript();
}
const contents = tree.read(modulePath).toString('utf-8');
const sourceFile = createSourceFile(
const sourceFile = tsModule.createSourceFile(
modulePath,
contents,
ScriptTarget.ESNext
tsModule.ScriptTarget.ESNext
);
const coreImport = findImport(sourceFile, '@angular/core');
@ -63,12 +58,14 @@ export function insertNgModuleProperty(
let ngModuleDecorator: Decorator;
try {
ngModuleClassDeclaration = findDecoratedClass(sourceFile, ngModuleName);
ngModuleDecorator = getDecorators(ngModuleClassDeclaration).find(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
ngModuleDecorator = tsModule
.getDecorators(ngModuleClassDeclaration)
.find(
(decorator) =>
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
} catch {
// Support for TS < 4.8
ngModuleClassDeclaration = findDecoratedClassLegacy(
@ -78,8 +75,8 @@ export function insertNgModuleProperty(
// @ts-ignore
ngModuleDecorator = ngModuleClassDeclaration.decorators.find(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
}
@ -96,7 +93,7 @@ export function insertNgModuleProperty(
]);
tree.write(modulePath, newContents);
} else {
if (!isObjectLiteralExpression(ngModuleCall.arguments[0])) {
if (!tsModule.isObjectLiteralExpression(ngModuleCall.arguments[0])) {
throw new Error(
`The NgModule options for ${ngModuleClassDeclaration.name.escapedText} in ${modulePath} is not an object literal`
);
@ -123,7 +120,7 @@ export function insertNgModuleProperty(
]);
tree.write(modulePath, newContents);
} else {
if (!isArrayLiteralExpression(typeProperty.initializer)) {
if (!tsModule.isArrayLiteralExpression(typeProperty.initializer)) {
throw new Error(
`The NgModule ${property} for ${ngModuleClassDeclaration.name.escapedText} in ${modulePath} is not an array literal`
);
@ -156,7 +153,13 @@ export function insertNgModuleImport(
}
function findImport(sourceFile: SourceFile, importPath: string) {
const importStatements = sourceFile.statements.filter(isImportDeclaration);
if (!tsModule) {
tsModule = ensureTypescript();
}
const importStatements = sourceFile.statements.filter(
tsModule.isImportDeclaration
);
return importStatements.find(
(statement) =>
@ -168,7 +171,11 @@ function findImport(sourceFile: SourceFile, importPath: string) {
}
function getNamedImport(coreImport: ImportDeclaration, importName: string) {
if (!isNamedImports(coreImport.importClause.namedBindings)) {
if (!tsModule) {
tsModule = ensureTypescript();
}
if (!tsModule.isNamedImports(coreImport.importClause.namedBindings)) {
throw new Error(
`The import from ${coreImport.moduleSpecifier} does not have named imports.`
);
@ -176,9 +183,9 @@ function getNamedImport(coreImport: ImportDeclaration, importName: string) {
return coreImport.importClause.namedBindings.elements.find((namedImport) =>
namedImport.propertyName
? isIdentifier(namedImport.propertyName) &&
? tsModule.isIdentifier(namedImport.propertyName) &&
namedImport.propertyName.escapedText === importName
: isIdentifier(namedImport.name) &&
: tsModule.isIdentifier(namedImport.name) &&
namedImport.name.escapedText === importName
);
}
@ -187,14 +194,20 @@ function findDecoratedClass(
sourceFile: SourceFile,
ngModuleName: __String
): ClassDeclaration | undefined {
const classDeclarations = sourceFile.statements.filter(isClassDeclaration);
if (!tsModule) {
tsModule = ensureTypescript();
}
const classDeclarations = sourceFile.statements.filter(
tsModule.isClassDeclaration
);
return classDeclarations.find((declaration) => {
const decorators = getDecorators(declaration);
const decorators = tsModule.getDecorators(declaration);
if (decorators) {
return decorators.some(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
}
@ -206,14 +219,20 @@ function findDecoratedClassLegacy(
sourceFile: SourceFile,
ngModuleName: __String
) {
const classDeclarations = sourceFile.statements.filter(isClassDeclaration);
if (!tsModule) {
tsModule = ensureTypescript();
}
const classDeclarations = sourceFile.statements.filter(
tsModule.isClassDeclaration
);
return classDeclarations.find(
(declaration) =>
declaration.decorators &&
(declaration.decorators as any[]).some(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
)
);
@ -223,10 +242,14 @@ function findPropertyAssignment(
ngModuleOptions: ObjectLiteralExpression,
propertyName: ngModuleDecoratorProperty
) {
if (!tsModule) {
tsModule = ensureTypescript();
}
return ngModuleOptions.properties.find(
(property) =>
isPropertyAssignment(property) &&
isIdentifier(property.name) &&
tsModule.isPropertyAssignment(property) &&
tsModule.isIdentifier(property.name) &&
property.name.escapedText === propertyName
) as PropertyAssignment;
}

View File

@ -1,4 +1,11 @@
import { joinPathFragments, workspaceRoot } from '@nrwl/devkit';
import {
joinPathFragments,
normalizePath,
readNxJson,
readProjectConfiguration,
Tree,
workspaceRoot,
} from '@nrwl/devkit';
import { basename, dirname, relative } from 'path';
export function pathStartsWith(path1: string, path2: string): boolean {
@ -22,3 +29,28 @@ export function getRelativeImportToFile(
basename(targetFilePath, '.ts')
)}`;
}
export function checkPathUnderProjectRoot(
tree: Tree,
projectName: string,
path: string
): void {
if (!path) {
return;
}
const project = projectName ?? readNxJson(tree).defaultProject;
const { root } = readProjectConfiguration(tree, project);
let pathToComponent = normalizePath(path);
pathToComponent = pathToComponent.startsWith('/')
? pathToComponent.slice(1)
: pathToComponent;
if (!pathStartsWith(pathToComponent, root)) {
throw new Error(
`The path provided (${path}) does not exist under the project root (${root}). ` +
`Please make sure to provide a path that exists under the project root.`
);
}
}