diff --git a/docs/design/analytics.md b/docs/design/analytics.md index 770acee03860..f66c036d8039 100644 --- a/docs/design/analytics.md +++ b/docs/design/analytics.md @@ -58,6 +58,7 @@ Note: There's a limit of 20 custom dimensions. | 12 | `Flag: --skip-tests` | `boolean` | | 13 | `Flag: --aot` | `boolean` | | 14 | `Flag: --minimal` | `boolean` | +| 15 | `Flag: --standalone` | `boolean` | | 16 | `Flag: --optimization` | `boolean` | | 17 | `Flag: --routing` | `boolean` | | 18 | `Flag: --skip-import` | `boolean` | diff --git a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.spec.ts.template b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.spec.ts.template index a769e2f7348a..74b3f67b9d40 100644 --- a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.spec.ts.template +++ b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.spec.ts.template @@ -8,7 +8,7 @@ describe('<%= classify(name) %><%= classify(type) %>', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ <%= classify(name) %><%= classify(type) %> ] + <%= standalone ? 'imports' : 'declarations' %>: [ <%= classify(name) %><%= classify(type) %> ] }) .compileComponents(); diff --git a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template index 442a76f98278..ce0dd0657ca0 100644 --- a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template +++ b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template @@ -1,7 +1,8 @@ import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core'; @Component({<% if(!skipSelector) {%> - selector: '<%= selector %>',<%}%><% if(inlineTemplate) { %> + selector: '<%= selector %>',<%}%><% if(standalone) {%> + standalone: true,<%}%><% if(inlineTemplate) { %> template: `

<%= dasherize(name) %> works! diff --git a/packages/schematics/angular/component/index.ts b/packages/schematics/angular/component/index.ts index 177b5eac276a..73ae57d03a97 100644 --- a/packages/schematics/angular/component/index.ts +++ b/packages/schematics/angular/component/index.ts @@ -43,7 +43,7 @@ function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile { function addDeclarationToNgModule(options: ComponentOptions): Rule { return (host: Tree) => { - if (options.skipImport || !options.module) { + if (options.skipImport || options.standalone || !options.module) { return host; } diff --git a/packages/schematics/angular/component/index_spec.ts b/packages/schematics/angular/component/index_spec.ts index 14235575b3b0..ad2fd381525d 100644 --- a/packages/schematics/angular/component/index_spec.ts +++ b/packages/schematics/angular/component/index_spec.ts @@ -420,4 +420,22 @@ describe('Component Schematic', () => { expect(content).toMatch(/template: `(\n(.|)*){3}\n\s*`,\n/); expect(content).toMatch(/changeDetection: ChangeDetectionStrategy.OnPush/); }); + + it('should create a standalone component', async () => { + const options = { ...defaultOptions, standalone: true }; + const tree = await schematicRunner.runSchematicAsync('component', options, appTree).toPromise(); + const moduleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); + const componentContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); + expect(componentContent).toContain('standalone: true'); + expect(componentContent).toContain('class FooComponent'); + expect(moduleContent).not.toContain('FooComponent'); + }); + + it('should declare standalone components in the `imports` of a test', async () => { + const options = { ...defaultOptions, standalone: true }; + const tree = await schematicRunner.runSchematicAsync('component', options, appTree).toPromise(); + const testContent = tree.readContent('/projects/bar/src/app/foo/foo.component.spec.ts'); + expect(testContent).toContain('imports: [ FooComponent ]'); + expect(testContent).not.toContain('declarations'); + }); }); diff --git a/packages/schematics/angular/component/schema.json b/packages/schematics/angular/component/schema.json index 115185cf9cb6..38b5d06435b7 100644 --- a/packages/schematics/angular/component/schema.json +++ b/packages/schematics/angular/component/schema.json @@ -48,6 +48,12 @@ "alias": "t", "x-user-analytics": 10 }, + "standalone": { + "description": "Whether the generated component is standalone.", + "type": "boolean", + "default": false, + "x-user-analytics": 15 + }, "viewEncapsulation": { "description": "The view encapsulation strategy to use in the new component.", "enum": ["Emulated", "None", "ShadowDom"], diff --git a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template b/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template index 6c8ce2d6531a..fcd41053e96b 100644 --- a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template +++ b/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template @@ -1,7 +1,8 @@ import { Directive } from '@angular/core'; @Directive({ - selector: '[<%= selector %>]' + selector: '[<%= selector %>]'<% if(standalone) {%>, + standalone: true<%}%> }) export class <%= classify(name) %>Directive { diff --git a/packages/schematics/angular/directive/index.ts b/packages/schematics/angular/directive/index.ts index 666a181b5b48..ef75f2506a92 100644 --- a/packages/schematics/angular/directive/index.ts +++ b/packages/schematics/angular/directive/index.ts @@ -31,7 +31,7 @@ import { Schema as DirectiveOptions } from './schema'; function addDeclarationToNgModule(options: DirectiveOptions): Rule { return (host: Tree) => { - if (options.skipImport || !options.module) { + if (options.skipImport || options.standalone || !options.module) { return host; } diff --git a/packages/schematics/angular/directive/index_spec.ts b/packages/schematics/angular/directive/index_spec.ts index 157a2352cf4f..160cade7d279 100644 --- a/packages/schematics/angular/directive/index_spec.ts +++ b/packages/schematics/angular/directive/index_spec.ts @@ -183,4 +183,14 @@ describe('Directive Schematic', () => { expect(files).toContain('/projects/bar/src/app/foo.directive.ts'); expect(files).not.toContain('/projects/bar/src/app/foo.directive.spec.ts'); }); + + it('should create a standalone directive', async () => { + const options = { ...defaultOptions, standalone: true }; + const tree = await schematicRunner.runSchematicAsync('directive', options, appTree).toPromise(); + const moduleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); + const directiveContent = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + expect(directiveContent).toContain('standalone: true'); + expect(directiveContent).toContain('class FooDirective'); + expect(moduleContent).not.toContain('FooDirective'); + }); }); diff --git a/packages/schematics/angular/directive/schema.json b/packages/schematics/angular/directive/schema.json index e56c131608f7..09ed861e6774 100644 --- a/packages/schematics/angular/directive/schema.json +++ b/packages/schematics/angular/directive/schema.json @@ -59,6 +59,12 @@ "format": "html-selector", "description": "The HTML selector to use for this directive." }, + "standalone": { + "description": "Whether the generated directive is standalone.", + "type": "boolean", + "default": false, + "x-user-analytics": 15 + }, "flat": { "type": "boolean", "description": "When true (the default), creates the new files at the top level of the current project.", diff --git a/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts.template b/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts.template index cd2a90149718..548764ac4f1e 100644 --- a/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts.template +++ b/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts.template @@ -1,7 +1,8 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: '<%= camelize(name) %>' + name: '<%= camelize(name) %>'<% if(standalone) {%>, + standalone: true<%}%> }) export class <%= classify(name) %>Pipe implements PipeTransform { diff --git a/packages/schematics/angular/pipe/index.ts b/packages/schematics/angular/pipe/index.ts index e18c432eabc2..014d32bc366b 100644 --- a/packages/schematics/angular/pipe/index.ts +++ b/packages/schematics/angular/pipe/index.ts @@ -30,7 +30,7 @@ import { Schema as PipeOptions } from './schema'; function addDeclarationToNgModule(options: PipeOptions): Rule { return (host: Tree) => { - if (options.skipImport || !options.module) { + if (options.skipImport || options.standalone || !options.module) { return host; } diff --git a/packages/schematics/angular/pipe/index_spec.ts b/packages/schematics/angular/pipe/index_spec.ts index 3dd75e667efa..66182c1202d0 100644 --- a/packages/schematics/angular/pipe/index_spec.ts +++ b/packages/schematics/angular/pipe/index_spec.ts @@ -147,4 +147,14 @@ describe('Pipe Schematic', () => { expect(files).not.toContain('/projects/bar/src/app/foo.pipe.spec.ts'); expect(files).toContain('/projects/bar/src/app/foo.pipe.ts'); }); + + it('should create a standalone pipe', async () => { + const options = { ...defaultOptions, standalone: true }; + const tree = await schematicRunner.runSchematicAsync('pipe', options, appTree).toPromise(); + const moduleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); + const pipeContent = tree.readContent('/projects/bar/src/app/foo.pipe.ts'); + expect(pipeContent).toContain('standalone: true'); + expect(pipeContent).toContain('class FooPipe'); + expect(moduleContent).not.toContain('FooPipe'); + }); }); diff --git a/packages/schematics/angular/pipe/schema.json b/packages/schematics/angular/pipe/schema.json index 230785ae3bb8..900dac7ccdb9 100644 --- a/packages/schematics/angular/pipe/schema.json +++ b/packages/schematics/angular/pipe/schema.json @@ -45,6 +45,12 @@ "description": "Do not import this pipe into the owning NgModule.", "x-user-analytics": 18 }, + "standalone": { + "description": "Whether the generated pipe is standalone.", + "type": "boolean", + "default": false, + "x-user-analytics": 15 + }, "module": { "type": "string", "description": "The declaring NgModule.", diff --git a/packages/schematics/angular/utility/find-module.ts b/packages/schematics/angular/utility/find-module.ts index a420f07eb052..112269d5e5da 100644 --- a/packages/schematics/angular/utility/find-module.ts +++ b/packages/schematics/angular/utility/find-module.ts @@ -17,6 +17,7 @@ export interface ModuleOptions { skipImport?: boolean; moduleExt?: string; routingModuleExt?: string; + standalone?: boolean; } export const MODULE_EXT = '.module.ts'; @@ -26,8 +27,7 @@ export const ROUTING_MODULE_EXT = '-routing.module.ts'; * Find the module referred by a set of options passed to the schematics. */ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path | undefined { - // eslint-disable-next-line no-prototype-builtins - if (options.hasOwnProperty('skipImport') && options.skipImport) { + if (options.standalone || options.skipImport) { return undefined; }