feat(angular): add scam generator for pipes and directives (#8144)

ISSUES CLOSED: #8122
This commit is contained in:
Shlomi Levi 2021-12-14 13:28:47 +02:00 committed by GitHub
parent bc8dda6c44
commit 4bb109e175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2080 additions and 0 deletions

View File

@ -0,0 +1,86 @@
---
title: '@nrwl/angular:scam-directive generator'
description: 'Generate a directive with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-directive
Generate a directive with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-directive ...
```
By default, Nx will search for `scam-directive` in the default collection provisioned in `angular.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-directive ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-directive ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the directive.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Directive.
### path (**hidden**)
Type: `string`
The path at which to create the directive file, relative to the current workspace. Default is a folder with the same name as the directive in the project root.
### prefix
Alias(es): p
Type: `string`
The prefix to apply to the generated directive selector.
### project
Type: `string`
The name of the project.
### selector
Type: `string`
The HTML selector to use for this directive.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new directive.

View File

@ -0,0 +1,72 @@
---
title: '@nrwl/angular:scam-pipe generator'
description: 'Generate a pipe with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-pipe
Generate a pipe with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-pipe ...
```
By default, Nx will search for `scam-pipe` in the default collection provisioned in `angular.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-pipe ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-pipe ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the pipe.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Pipe.
### path (**hidden**)
Type: `string`
The path at which to create the pipe file, relative to the current workspace. Default is a folder with the same name as the pipe in the project root.
### project
Type: `string`
The name of the project.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new pipe.

View File

@ -473,6 +473,16 @@
"id": "scam", "id": "scam",
"file": "angular/api-angular/generators/scam" "file": "angular/api-angular/generators/scam"
}, },
{
"name": "scam-directive generator",
"id": "scam-directive",
"file": "angular/api-angular/generators/scam-directive"
},
{
"name": "scam-pipe generator",
"id": "scam-pipe",
"file": "angular/api-angular/generators/scam-pipe"
},
{ {
"name": "setup-mfe generator", "name": "setup-mfe generator",
"id": "setup-mfe", "id": "setup-mfe",
@ -1812,6 +1822,16 @@
"id": "scam", "id": "scam",
"file": "react/api-angular/generators/scam" "file": "react/api-angular/generators/scam"
}, },
{
"name": "scam-directive generator",
"id": "scam-directive",
"file": "react/api-angular/generators/scam-directive"
},
{
"name": "scam-pipe generator",
"id": "scam-pipe",
"file": "react/api-angular/generators/scam-pipe"
},
{ {
"name": "setup-mfe generator", "name": "setup-mfe generator",
"id": "setup-mfe", "id": "setup-mfe",
@ -3115,6 +3135,16 @@
"id": "scam", "id": "scam",
"file": "node/api-angular/generators/scam" "file": "node/api-angular/generators/scam"
}, },
{
"name": "scam-directive generator",
"id": "scam-directive",
"file": "node/api-angular/generators/scam-directive"
},
{
"name": "scam-pipe generator",
"id": "scam-pipe",
"file": "node/api-angular/generators/scam-pipe"
},
{ {
"name": "setup-mfe generator", "name": "setup-mfe generator",
"id": "setup-mfe", "id": "setup-mfe",

View File

@ -0,0 +1,86 @@
---
title: '@nrwl/angular:scam-directive generator'
description: 'Generate a directive with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-directive
Generate a directive with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-directive ...
```
By default, Nx will search for `scam-directive` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-directive ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-directive ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the directive.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Directive.
### path (**hidden**)
Type: `string`
The path at which to create the directive file, relative to the current workspace. Default is a folder with the same name as the directive in the project root.
### prefix
Alias(es): p
Type: `string`
The prefix to apply to the generated directive selector.
### project
Type: `string`
The name of the project.
### selector
Type: `string`
The HTML selector to use for this directive.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new directive.

View File

@ -0,0 +1,72 @@
---
title: '@nrwl/angular:scam-pipe generator'
description: 'Generate a pipe with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-pipe
Generate a pipe with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-pipe ...
```
By default, Nx will search for `scam-pipe` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-pipe ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-pipe ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the pipe.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Pipe.
### path (**hidden**)
Type: `string`
The path at which to create the pipe file, relative to the current workspace. Default is a folder with the same name as the pipe in the project root.
### project
Type: `string`
The name of the project.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new pipe.

View File

@ -0,0 +1,86 @@
---
title: '@nrwl/angular:scam-directive generator'
description: 'Generate a directive with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-directive
Generate a directive with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-directive ...
```
By default, Nx will search for `scam-directive` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-directive ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-directive ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the directive.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Directive.
### path (**hidden**)
Type: `string`
The path at which to create the directive file, relative to the current workspace. Default is a folder with the same name as the directive in the project root.
### prefix
Alias(es): p
Type: `string`
The prefix to apply to the generated directive selector.
### project
Type: `string`
The name of the project.
### selector
Type: `string`
The HTML selector to use for this directive.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new directive.

View File

@ -0,0 +1,72 @@
---
title: '@nrwl/angular:scam-pipe generator'
description: 'Generate a pipe with an accompanying Single Component Angular Module (SCAM).'
---
# @nrwl/angular:scam-pipe
Generate a pipe with an accompanying Single Component Angular Module (SCAM).
## Usage
```bash
nx generate scam-pipe ...
```
By default, Nx will search for `scam-pipe` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:scam-pipe ...
```
Show what will be generated without writing to disk:
```bash
nx g scam-pipe ... --dry-run
```
## Options
### name (_**required**_)
Type: `string`
The name of the pipe.
### flat
Default: `true`
Type: `boolean`
Create the new files at the top level of the current project.
### inlineScam
Default: `true`
Type: `boolean`
Create the NgModule in the same file as the Pipe.
### path (**hidden**)
Type: `string`
The path at which to create the pipe file, relative to the current workspace. Default is a folder with the same name as the pipe in the project root.
### project
Type: `string`
The name of the project.
### skipTests
Default: `false`
Type: `boolean`
Do not create "spec.ts" test files for the new pipe.

View File

@ -115,6 +115,16 @@
"schema": "./src/generators/scam/schema.json", "schema": "./src/generators/scam/schema.json",
"description": "Generate a component with an accompanying Single Component Angular Module (SCAM)." "description": "Generate a component with an accompanying Single Component Angular Module (SCAM)."
}, },
"scam-directive": {
"factory": "./src/generators/scam-directive/scam-directive.compat",
"schema": "./src/generators/scam-directive/schema.json",
"description": "Generate a directive with an accompanying Single Component Angular Module (SCAM)."
},
"scam-pipe": {
"factory": "./src/generators/scam-pipe/scam-pipe.compat",
"schema": "./src/generators/scam-pipe/schema.json",
"description": "Generate a pipe with an accompanying Single Component Angular Module (SCAM)."
},
"web-worker": { "web-worker": {
"factory": "./src/generators/web-worker/compat", "factory": "./src/generators/web-worker/compat",
@ -209,6 +219,16 @@
"schema": "./src/generators/scam/schema.json", "schema": "./src/generators/scam/schema.json",
"description": "Generate a component with an accompanying Single Component Angular Module (SCAM)." "description": "Generate a component with an accompanying Single Component Angular Module (SCAM)."
}, },
"scam-directive": {
"factory": "./src/generators/scam-directive/scam-directive",
"schema": "./src/generators/scam-directive/schema.json",
"description": "Generate a directive with an accompanying Single Component Angular Module (SCAM)."
},
"scam-pipe": {
"factory": "./src/generators/scam-pipe/scam-pipe",
"schema": "./src/generators/scam-pipe/schema.json",
"description": "Generate a pipe with an accompanying Single Component Angular Module (SCAM)."
},
"stories": { "stories": {
"factory": "./src/generators/stories/stories", "factory": "./src/generators/stories/stories",
"schema": "./src/generators/stories/schema.json", "schema": "./src/generators/stories/schema.json",

View File

@ -15,3 +15,5 @@ export * from './src/generators/storybook-migrate-stories-to-6-2/migrate-stories
export * from './src/generators/upgrade-module/upgrade-module'; export * from './src/generators/upgrade-module/upgrade-module';
export * from './src/generators/setup-mfe/setup-mfe'; export * from './src/generators/setup-mfe/setup-mfe';
export * from './src/generators/scam/scam'; export * from './src/generators/scam/scam';
export * from './src/generators/scam-directive/scam-directive';
export * from './src/generators/scam-pipe/scam-pipe';

View File

@ -0,0 +1,331 @@
import { addProjectConfiguration } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createScamDirective } from './create-module';
describe('Create module in the tree', () => {
it('should create the scam directive inline correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: false,
});
// ASSERT
const directiveSource = tree.read(
'apps/app1/src/app/example/example.directive.ts',
'utf-8'
);
expect(directiveSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should create the scam directive separately correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: false,
});
// ASSERT
const directiveModuleSource = tree.read(
'apps/app1/src/app/example/example.module.ts',
'utf-8'
);
expect(directiveModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExampleDirective } from './example.directive';
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should create the scam directive inline correctly when --flat', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: true,
});
// ASSERT
const directiveSource = tree.read(
'apps/app1/src/app/example.directive.ts',
'utf-8'
);
expect(directiveSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should create the scam directive separately correctly when --flat', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: true,
});
// ASSERT
const directiveModuleSource = tree.read(
'apps/app1/src/app/example.module.ts',
'utf-8'
);
expect(directiveModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExampleDirective } from './example.directive';
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should place the directive and scam in the correct folder when --path is used', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
path: 'apps/app1/src/app/random',
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
flat: false,
path: 'apps/app1/src/app/random',
inlineScam: true,
});
// ASSERT
const directiveModuleSource = tree.read(
'apps/app1/src/app/random/example/example.directive.ts',
'utf-8'
);
expect(directiveModuleSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should place the directive and scam in the correct folder when --path and --flat is used', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
path: 'apps/app1/src/app/random',
});
// ACT
createScamDirective(tree, {
name: 'example',
project: 'app1',
flat: true,
path: 'apps/app1/src/app/random',
inlineScam: true,
});
// ASSERT
const directiveModuleSource = tree.read(
'apps/app1/src/app/random/example.directive.ts',
'utf-8'
);
expect(directiveModuleSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
});

View File

@ -0,0 +1,117 @@
import type { Tree } from '@nrwl/devkit';
import type { Schema } from '../schema';
import {
readProjectConfiguration,
joinPathFragments,
names,
readWorkspaceConfiguration,
normalizePath,
} from '@nrwl/devkit';
import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils';
import { createSourceFile, ScriptTarget } from 'typescript';
export function createScamDirective(tree: Tree, schema: Schema) {
const project =
schema.project ?? readWorkspaceConfiguration(tree).defaultProject;
const projectConfig = readProjectConfiguration(tree, project);
const directiveNames = names(schema.name);
const typeNames = names('directive');
const directiveFileName = `${directiveNames.fileName}.directive`;
let directiveDirectory = schema.flat
? joinPathFragments(
projectConfig.sourceRoot,
projectConfig.projectType === 'application' ? 'app' : 'lib'
)
: joinPathFragments(
projectConfig.sourceRoot,
projectConfig.projectType === 'application' ? 'app' : 'lib',
directiveNames.fileName
);
if (schema.path) {
directiveDirectory = schema.flat
? normalizePath(schema.path)
: joinPathFragments(schema.path, directiveNames.fileName);
}
const directiveFilePath = joinPathFragments(
directiveDirectory,
`${directiveFileName}.ts`
);
if (!tree.exists(directiveFilePath)) {
throw new Error(
`Couldn't find directive at path ${directiveFilePath} to add SCAM setup.`
);
}
if (schema.inlineScam) {
const currentDirectiveContents = tree.read(directiveFilePath, 'utf-8');
let source = createSourceFile(
directiveFilePath,
currentDirectiveContents,
ScriptTarget.Latest,
true
);
source = insertImport(
tree,
source,
directiveFilePath,
'NgModule',
'@angular/core'
);
source = insertImport(
tree,
source,
directiveFilePath,
'CommonModule',
'@angular/common'
);
let updatedDirectiveSource = source.getText();
updatedDirectiveSource = `${updatedDirectiveSource}${createAngularDirectiveModule(
`${directiveNames.className}${typeNames.className}`
)}`;
tree.write(directiveFilePath, updatedDirectiveSource);
return;
}
tree.write(
joinPathFragments(
directiveDirectory,
`${directiveNames.fileName}.module.ts`
),
createSeparateAngularDirectiveModuleFile(
`${directiveNames.className}${typeNames.className}`,
directiveFileName
)
);
}
function createAngularDirectiveModule(name: string) {
return `
@NgModule({
imports: [CommonModule],
declarations: [${name}],
exports: [${name}],
})
export class ${name}Module {}`;
}
function createSeparateAngularDirectiveModuleFile(
name: string,
directiveFileName: string
) {
return `import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ${name} } from './${directiveFileName}';
${createAngularDirectiveModule(name)}`;
}

View File

@ -0,0 +1,4 @@
import scamGenerator from './scam-directive';
import { convertNxGenerator } from '@nrwl/devkit';
export default convertNxGenerator(scamGenerator);

View File

@ -0,0 +1,203 @@
import { addProjectConfiguration } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import scamDirectiveGenerator from './scam-directive';
describe('SCAM Directive Generator', () => {
it('should create the inline scam directive correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamDirectiveGenerator(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: true,
});
// ASSERT
const directiveSource = tree.read(
'apps/app1/src/app/example.directive.ts',
'utf-8'
);
expect(directiveSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should create the separate scam directive correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamDirectiveGenerator(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: true,
});
// ASSERT
const directiveModuleSource = tree.read(
'apps/app1/src/app/example.module.ts',
'utf-8'
);
expect(directiveModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExampleDirective } from './example.directive';
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
describe('--path', () => {
it('should not throw when the path does not exist under project', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamDirectiveGenerator(tree, {
name: 'example',
project: 'app1',
path: 'apps/app1/src/app/random',
inlineScam: true,
flat: false,
});
// ASSERT
const directiveSource = tree.read(
'apps/app1/src/app/random/example/example.directive.ts',
'utf-8'
);
expect(directiveSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should not matter if the path starts with a slash', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamDirectiveGenerator(tree, {
name: 'example',
project: 'app1',
path: '/apps/app1/src/app/random',
inlineScam: true,
flat: false,
});
// ASSERT
const directiveSource = tree.read(
'apps/app1/src/app/random/example/example.directive.ts',
'utf-8'
);
expect(directiveSource).toMatchInlineSnapshot(`
"import { Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[example]'
})
export class ExampleDirective {
constructor() { }
}
@NgModule({
imports: [CommonModule],
declarations: [ExampleDirective],
exports: [ExampleDirective],
})
export class ExampleDirectiveModule {}"
`);
});
it('should throw when the path does not exist under project', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
try {
await scamDirectiveGenerator(tree, {
name: 'example',
project: 'app1',
path: 'libs/proj/src/lib/random',
inlineScam: true,
flat: false,
});
} catch (error) {
// ASSERT
expect(error).toMatchInlineSnapshot(
`[Error: The path provided for the SCAM (libs/proj/src/lib/random) does not exist under the project root (apps/app1).]`
);
}
});
});
});

View File

@ -0,0 +1,54 @@
import type { Tree } from '@nrwl/devkit';
import type { Schema } from './schema';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import {
formatFiles,
readWorkspaceConfiguration,
readProjectConfiguration,
normalizePath,
} from '@nrwl/devkit';
import { createScamDirective } from './lib/create-module';
import { normalize } from 'path';
export async function scamDirectiveGenerator(tree: Tree, schema: Schema) {
const { inlineScam, ...options } = schema;
checkPathUnderProjectRoot(tree, options);
const angularDirectiveSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'directive'
);
await angularDirectiveSchematic(tree, {
...options,
skipImport: true,
export: false,
});
createScamDirective(tree, schema);
await formatFiles(tree);
}
function checkPathUnderProjectRoot(tree: Tree, options: Partial<Schema>) {
if (!options.path) {
return;
}
const project =
options.project ?? readWorkspaceConfiguration(tree).defaultProject;
const { root } = readProjectConfiguration(tree, project);
let pathToDirective = normalizePath(options.path);
pathToDirective = pathToDirective.startsWith('/')
? pathToDirective.slice(1)
: pathToDirective;
if (!pathToDirective.startsWith(normalize(root))) {
throw new Error(
`The path provided for the SCAM (${options.path}) does not exist under the project root (${root}).`
);
}
}
export default scamDirectiveGenerator;

View File

@ -0,0 +1,10 @@
export interface Schema {
name: string;
path?: string;
project?: string;
skipTests?: boolean;
inlineScam?: boolean;
flat?: boolean;
prefix?: string;
selector?: string;
}

View File

@ -0,0 +1,68 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "SCAMDirectiveGenerator",
"cli": "nx",
"title": "SCAM Directive Generator Options Schema",
"type": "object",
"description": "Creates a new, generic directive definition in the given or default project.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"format": "path",
"description": "The path at which to create the directive file, relative to the current workspace. Default is a folder with the same name as the directive in the project root.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"name": {
"type": "string",
"description": "The name of the directive.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the directive?"
},
"skipTests": {
"type": "boolean",
"description": "Do not create \"spec.ts\" test files for the new directive.",
"default": false
},
"inlineScam": {
"type": "boolean",
"description": "Create the NgModule in the same file as the Directive.",
"default": true
},
"flat": {
"type": "boolean",
"description": "Create the new files at the top level of the current project.",
"default": true
},
"selector": {
"type": "string",
"format": "html-selector",
"description": "The HTML selector to use for this directive."
},
"prefix": {
"type": "string",
"description": "The prefix to apply to the generated directive selector.",
"alias": "p",
"oneOf": [
{
"maxLength": 0
},
{
"minLength": 1,
"format": "html-selector"
}
]
}
},
"required": ["name"]
}

View File

@ -0,0 +1,335 @@
import { addProjectConfiguration } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createScamPipe } from './create-module';
describe('Create module in the tree', () => {
it('should create the scam pipe inline correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: false,
});
// ASSERT
const pipeSource = tree.read(
'apps/app1/src/app/example/example.pipe.ts',
'utf-8'
);
expect(pipeSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should create the scam pipe separately correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: false,
});
// ASSERT
const pipeModuleSource = tree.read(
'apps/app1/src/app/example/example.module.ts',
'utf-8'
);
expect(pipeModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExamplePipe } from './example.pipe';
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should create the scam pipe inline correctly when --flat', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: true,
});
// ASSERT
const pipeSource = tree.read('apps/app1/src/app/example.pipe.ts', 'utf-8');
expect(pipeSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should create the scam pipe separately correctly when --flat', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: true,
});
// ASSERT
const pipeModuleSource = tree.read(
'apps/app1/src/app/example.module.ts',
'utf-8'
);
expect(pipeModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExamplePipe } from './example.pipe';
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should place the pipe and scam pipe in the correct folder when --path is used', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: false,
path: 'apps/app1/src/app/random',
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
flat: false,
path: 'apps/app1/src/app/random',
inlineScam: true,
});
// ASSERT
const pipeModuleSource = tree.read(
'apps/app1/src/app/random/example/example.pipe.ts',
'utf-8'
);
expect(pipeModuleSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should place the pipe and scam pipe in the correct folder when --path and --flat is used', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
const angularComponentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularComponentSchematic(tree, {
name: 'example',
project: 'app1',
skipImport: true,
export: false,
flat: true,
path: 'apps/app1/src/app/random',
});
// ACT
createScamPipe(tree, {
name: 'example',
project: 'app1',
flat: true,
path: 'apps/app1/src/app/random',
inlineScam: true,
});
// ASSERT
const pipeModuleSource = tree.read(
'apps/app1/src/app/random/example.pipe.ts',
'utf-8'
);
expect(pipeModuleSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
});

View File

@ -0,0 +1,111 @@
import type { Tree } from '@nrwl/devkit';
import type { Schema } from '../schema';
import {
readProjectConfiguration,
joinPathFragments,
names,
readWorkspaceConfiguration,
normalizePath,
} from '@nrwl/devkit';
import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils';
import { createSourceFile, ScriptTarget } from 'typescript';
export function createScamPipe(tree: Tree, schema: Schema) {
const project =
schema.project ?? readWorkspaceConfiguration(tree).defaultProject;
const projectConfig = readProjectConfiguration(tree, project);
const pipeNames = names(schema.name);
const typeNames = names('pipe');
const pipeFileName = `${pipeNames.fileName}.pipe`;
let pipeDirectory = schema.flat
? joinPathFragments(
projectConfig.sourceRoot,
projectConfig.projectType === 'application' ? 'app' : 'lib'
)
: joinPathFragments(
projectConfig.sourceRoot,
projectConfig.projectType === 'application' ? 'app' : 'lib',
pipeNames.fileName
);
if (schema.path) {
pipeDirectory = schema.flat
? normalizePath(schema.path)
: joinPathFragments(schema.path, pipeNames.fileName);
}
const pipeFilePath = joinPathFragments(pipeDirectory, `${pipeFileName}.ts`);
if (!tree.exists(pipeFilePath)) {
throw new Error(
`Couldn't find pipe at path ${pipeFilePath} to add SCAM setup.`
);
}
if (schema.inlineScam) {
const currentPipeContents = tree.read(pipeFilePath, 'utf-8');
let source = createSourceFile(
pipeFilePath,
currentPipeContents,
ScriptTarget.Latest,
true
);
source = insertImport(
tree,
source,
pipeFilePath,
'NgModule',
'@angular/core'
);
source = insertImport(
tree,
source,
pipeFilePath,
'CommonModule',
'@angular/common'
);
let updatedPipeSource = source.getText();
updatedPipeSource = `${updatedPipeSource}${createAngularPipeModule(
`${pipeNames.className}${typeNames.className}`
)}`;
tree.write(pipeFilePath, updatedPipeSource);
return;
}
tree.write(
joinPathFragments(pipeDirectory, `${pipeNames.fileName}.module.ts`),
createSeparateAngularPipeModuleFile(
`${pipeNames.className}${typeNames.className}`,
pipeFileName
)
);
}
function createAngularPipeModule(name: string) {
return `
@NgModule({
imports: [CommonModule],
declarations: [${name}],
exports: [${name}],
})
export class ${name}Module {}`;
}
function createSeparateAngularPipeModuleFile(
name: string,
pipeFileName: string
) {
return `import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ${name} } from './${pipeFileName}';
${createAngularPipeModule(name)}`;
}

View File

@ -0,0 +1,4 @@
import scamPipeGenerator from './scam-pipe';
import { convertNxGenerator } from '@nrwl/devkit';
export default convertNxGenerator(scamPipeGenerator);

View File

@ -0,0 +1,208 @@
import { addProjectConfiguration } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import scamPipeGenerator from './scam-pipe';
describe('SCAM Pipe Generator', () => {
it('should create the inline scam pipe correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamPipeGenerator(tree, {
name: 'example',
project: 'app1',
inlineScam: true,
flat: false,
});
// ASSERT
const pipeSource = tree.read(
'apps/app1/src/app/example/example.pipe.ts',
'utf-8'
);
expect(pipeSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should create the separate scam pipe correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamPipeGenerator(tree, {
name: 'example',
project: 'app1',
inlineScam: false,
flat: false,
});
// ASSERT
const pipeModuleSource = tree.read(
'apps/app1/src/app/example/example.module.ts',
'utf-8'
);
expect(pipeModuleSource).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExamplePipe } from './example.pipe';
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
describe('--path', () => {
it('should not throw when the path does not exist under project', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamPipeGenerator(tree, {
name: 'example',
project: 'app1',
path: 'apps/app1/src/app/random',
inlineScam: true,
flat: false,
});
// ASSERT
const pipeSource = tree.read(
'apps/app1/src/app/random/example/example.pipe.ts',
'utf-8'
);
expect(pipeSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should not matter if the path starts with a slash', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
await scamPipeGenerator(tree, {
name: 'example',
project: 'app1',
path: '/apps/app1/src/app/random',
inlineScam: true,
flat: false,
});
// ASSERT
const pipeSource = tree.read(
'apps/app1/src/app/random/example/example.pipe.ts',
'utf-8'
);
expect(pipeSource).toMatchInlineSnapshot(`
"import { Pipe, PipeTransform, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Pipe({
name: 'example'
})
export class ExamplePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
@NgModule({
imports: [CommonModule],
declarations: [ExamplePipe],
exports: [ExamplePipe],
})
export class ExamplePipeModule {}"
`);
});
it('should throw when the path does not exist under project', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace(2);
addProjectConfiguration(tree, 'app1', {
projectType: 'application',
sourceRoot: 'apps/app1/src',
root: 'apps/app1',
});
// ACT
try {
await scamPipeGenerator(tree, {
name: 'example',
project: 'app1',
path: 'libs/proj/src/lib/random',
inlineScam: true,
});
} catch (error) {
// ASSERT
expect(error).toMatchInlineSnapshot(
`[Error: The path provided for the SCAM (libs/proj/src/lib/random) does not exist under the project root (apps/app1).]`
);
}
});
});
});

View File

@ -0,0 +1,52 @@
import type { Tree } from '@nrwl/devkit';
import type { Schema } from './schema';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import {
formatFiles,
readWorkspaceConfiguration,
readProjectConfiguration,
normalizePath,
} from '@nrwl/devkit';
import { createScamPipe } from './lib/create-module';
import { normalize } from 'path';
export async function scamPipeGenerator(tree: Tree, schema: Schema) {
const { inlineScam, ...options } = schema;
checkPathUnderProjectRoot(tree, options);
const angularPipeSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'pipe'
);
await angularPipeSchematic(tree, {
...options,
skipImport: true,
export: false,
});
createScamPipe(tree, schema);
await formatFiles(tree);
}
function checkPathUnderProjectRoot(tree: Tree, options: Partial<Schema>) {
if (!options.path) {
return;
}
const project =
options.project ?? readWorkspaceConfiguration(tree).defaultProject;
const { root } = readProjectConfiguration(tree, project);
let pathToPipe = normalizePath(options.path);
pathToPipe = pathToPipe.startsWith('/') ? pathToPipe.slice(1) : pathToPipe;
if (!pathToPipe.startsWith(normalize(root))) {
throw new Error(
`The path provided for the SCAM (${options.path}) does not exist under the project root (${root}).`
);
}
}
export default scamPipeGenerator;

View File

@ -0,0 +1,8 @@
export interface Schema {
name: string;
path?: string;
project?: string;
skipTests?: boolean;
inlineScam?: boolean;
flat?: boolean;
}

View File

@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "SCAMPipeGenerator",
"cli": "nx",
"title": "SCAM Pipe Generator Options Schema",
"type": "object",
"description": "Creates a new, generic pipe definition in the given or default project.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"format": "path",
"description": "The path at which to create the pipe file, relative to the current workspace. Default is a folder with the same name as the pipe in the project root.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"name": {
"type": "string",
"description": "The name of the pipe.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the pipe?"
},
"skipTests": {
"type": "boolean",
"description": "Do not create \"spec.ts\" test files for the new pipe.",
"default": false
},
"inlineScam": {
"type": "boolean",
"description": "Create the NgModule in the same file as the Pipe.",
"default": true
},
"flat": {
"type": "boolean",
"description": "Create the new files at the top level of the current project.",
"default": true
}
},
"required": ["name"]
}