diff --git a/docs/angular/api-angular/generators/scam-directive.md b/docs/angular/api-angular/generators/scam-directive.md new file mode 100644 index 0000000000..e1cf7902d8 --- /dev/null +++ b/docs/angular/api-angular/generators/scam-directive.md @@ -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. diff --git a/docs/angular/api-angular/generators/scam-pipe.md b/docs/angular/api-angular/generators/scam-pipe.md new file mode 100644 index 0000000000..fe147da0b2 --- /dev/null +++ b/docs/angular/api-angular/generators/scam-pipe.md @@ -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. diff --git a/docs/map.json b/docs/map.json index 0493406ecd..11f1ec8547 100644 --- a/docs/map.json +++ b/docs/map.json @@ -473,6 +473,16 @@ "id": "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", "id": "setup-mfe", @@ -1812,6 +1822,16 @@ "id": "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", "id": "setup-mfe", @@ -3115,6 +3135,16 @@ "id": "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", "id": "setup-mfe", diff --git a/docs/node/api-angular/generators/scam-directive.md b/docs/node/api-angular/generators/scam-directive.md new file mode 100644 index 0000000000..cb067fd1d4 --- /dev/null +++ b/docs/node/api-angular/generators/scam-directive.md @@ -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. diff --git a/docs/node/api-angular/generators/scam-pipe.md b/docs/node/api-angular/generators/scam-pipe.md new file mode 100644 index 0000000000..877cfb04a4 --- /dev/null +++ b/docs/node/api-angular/generators/scam-pipe.md @@ -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. diff --git a/docs/react/api-angular/generators/scam-directive.md b/docs/react/api-angular/generators/scam-directive.md new file mode 100644 index 0000000000..cb067fd1d4 --- /dev/null +++ b/docs/react/api-angular/generators/scam-directive.md @@ -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. diff --git a/docs/react/api-angular/generators/scam-pipe.md b/docs/react/api-angular/generators/scam-pipe.md new file mode 100644 index 0000000000..877cfb04a4 --- /dev/null +++ b/docs/react/api-angular/generators/scam-pipe.md @@ -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. diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 4688a2b45b..97f2cc6607 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -115,6 +115,16 @@ "schema": "./src/generators/scam/schema.json", "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": { "factory": "./src/generators/web-worker/compat", @@ -209,6 +219,16 @@ "schema": "./src/generators/scam/schema.json", "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": { "factory": "./src/generators/stories/stories", "schema": "./src/generators/stories/schema.json", diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index e4f3dcc865..4c42585281 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -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/setup-mfe/setup-mfe'; export * from './src/generators/scam/scam'; +export * from './src/generators/scam-directive/scam-directive'; +export * from './src/generators/scam-pipe/scam-pipe'; diff --git a/packages/angular/src/generators/scam-directive/lib/create-module.spec.ts b/packages/angular/src/generators/scam-directive/lib/create-module.spec.ts new file mode 100644 index 0000000000..48fafd4c5f --- /dev/null +++ b/packages/angular/src/generators/scam-directive/lib/create-module.spec.ts @@ -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 {}" + `); + }); +}); diff --git a/packages/angular/src/generators/scam-directive/lib/create-module.ts b/packages/angular/src/generators/scam-directive/lib/create-module.ts new file mode 100644 index 0000000000..10962ccdf6 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/lib/create-module.ts @@ -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)}`; +} diff --git a/packages/angular/src/generators/scam-directive/lib/index.ts b/packages/angular/src/generators/scam-directive/lib/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/angular/src/generators/scam-directive/scam-directive.compat.ts b/packages/angular/src/generators/scam-directive/scam-directive.compat.ts new file mode 100644 index 0000000000..94e4962f95 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/scam-directive.compat.ts @@ -0,0 +1,4 @@ +import scamGenerator from './scam-directive'; +import { convertNxGenerator } from '@nrwl/devkit'; + +export default convertNxGenerator(scamGenerator); diff --git a/packages/angular/src/generators/scam-directive/scam-directive.spec.ts b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts new file mode 100644 index 0000000000..8e5edf1ed7 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts @@ -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).]` + ); + } + }); + }); +}); diff --git a/packages/angular/src/generators/scam-directive/scam-directive.ts b/packages/angular/src/generators/scam-directive/scam-directive.ts new file mode 100644 index 0000000000..8a550d5949 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/scam-directive.ts @@ -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) { + 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; diff --git a/packages/angular/src/generators/scam-directive/schema.d.ts b/packages/angular/src/generators/scam-directive/schema.d.ts new file mode 100644 index 0000000000..9103c41db2 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/schema.d.ts @@ -0,0 +1,10 @@ +export interface Schema { + name: string; + path?: string; + project?: string; + skipTests?: boolean; + inlineScam?: boolean; + flat?: boolean; + prefix?: string; + selector?: string; +} diff --git a/packages/angular/src/generators/scam-directive/schema.json b/packages/angular/src/generators/scam-directive/schema.json new file mode 100644 index 0000000000..d9ac311240 --- /dev/null +++ b/packages/angular/src/generators/scam-directive/schema.json @@ -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"] +} diff --git a/packages/angular/src/generators/scam-pipe/lib/create-module.spec.ts b/packages/angular/src/generators/scam-pipe/lib/create-module.spec.ts new file mode 100644 index 0000000000..5a733bc344 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/lib/create-module.spec.ts @@ -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 {}" + `); + }); +}); diff --git a/packages/angular/src/generators/scam-pipe/lib/create-module.ts b/packages/angular/src/generators/scam-pipe/lib/create-module.ts new file mode 100644 index 0000000000..175db9bc13 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/lib/create-module.ts @@ -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)}`; +} diff --git a/packages/angular/src/generators/scam-pipe/lib/index.ts b/packages/angular/src/generators/scam-pipe/lib/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/angular/src/generators/scam-pipe/scam-pipe.compat.ts b/packages/angular/src/generators/scam-pipe/scam-pipe.compat.ts new file mode 100644 index 0000000000..e166791d61 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/scam-pipe.compat.ts @@ -0,0 +1,4 @@ +import scamPipeGenerator from './scam-pipe'; +import { convertNxGenerator } from '@nrwl/devkit'; + +export default convertNxGenerator(scamPipeGenerator); diff --git a/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts b/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts new file mode 100644 index 0000000000..ff88977876 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts @@ -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).]` + ); + } + }); + }); +}); diff --git a/packages/angular/src/generators/scam-pipe/scam-pipe.ts b/packages/angular/src/generators/scam-pipe/scam-pipe.ts new file mode 100644 index 0000000000..e0eb7f5c71 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/scam-pipe.ts @@ -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) { + 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; diff --git a/packages/angular/src/generators/scam-pipe/schema.d.ts b/packages/angular/src/generators/scam-pipe/schema.d.ts new file mode 100644 index 0000000000..7f6ff061ac --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/schema.d.ts @@ -0,0 +1,8 @@ +export interface Schema { + name: string; + path?: string; + project?: string; + skipTests?: boolean; + inlineScam?: boolean; + flat?: boolean; +} diff --git a/packages/angular/src/generators/scam-pipe/schema.json b/packages/angular/src/generators/scam-pipe/schema.json new file mode 100644 index 0000000000..0887f34aa1 --- /dev/null +++ b/packages/angular/src/generators/scam-pipe/schema.json @@ -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"] +}