Skip to content

Commit

Permalink
feat(schematics): add helper for adding destroy subject to Angular ar…
Browse files Browse the repository at this point in the history
…tifact
  • Loading branch information
dhhyi committed Jun 4, 2020
1 parent 0452353 commit 4013ff6
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
85 changes: 85 additions & 0 deletions schematics/src/add-destroy-subject-to-component/factory.ts
@@ -0,0 +1,85 @@
import { Rule, SchematicsException, chain } from '@angular-devkit/schematics';
import { buildDefaultPath, getProject } from '@schematics/angular/utility/project';
import { Scope } from 'ts-morph';

import { applyLintFix } from '../utils/lint-fix';
import { createTsMorphProject } from '../utils/ts-morph';

import { PWAAddDestroySubjectToComponentOptionsSchema as Options } from './schema';

export function add(options: Options): Rule {
return host => {
if (!options.project) {
throw new SchematicsException('Option (project) is required.');
}
let path = `${buildDefaultPath(getProject(host, options.project))}/${options.name
.replace(/.*src\/app\//, '')
.replace(/\/$/, '')}`;

if (!path.endsWith('.ts') && host.getDir(path)) {
const file = host.getDir(path).subfiles.find(el => /(component|pipe|directive)\.ts/.test(el));
path = `${path}/${file}`;
}

if (!path || !host.exists(path)) {
throw new SchematicsException('Option (path) is required and must exist.');
}

const tsMorphProject = createTsMorphProject(host);
tsMorphProject.addSourceFileAtPath(path);

const sourceFile = tsMorphProject.getSourceFile(path);
sourceFile
.getClasses()
.filter(clazz => clazz.isExported())
.forEach(classDeclaration => {
if (!classDeclaration.getImplements().find(imp => imp.getText() === 'OnDestroy')) {
classDeclaration.addImplements('OnDestroy');
}

if (!classDeclaration.getProperty('destroy$')) {
classDeclaration.insertProperty(classDeclaration.getProperties().length, {
name: 'destroy$',
initializer: 'new Subject()',
scope: Scope.Private,
});
}

if (!classDeclaration.getMethod('ngOnDestroy')) {
const onDestroyMethod = classDeclaration.addMethod({
name: 'ngOnDestroy',
});

const methodBody = onDestroyMethod.addBody();
methodBody.setBodyText(`this.destroy$.next();
this.destroy$.complete();`);
}
});

const angularCoreImport = sourceFile.getImportDeclarationOrThrow(
imp => imp.getModuleSpecifierValue() === '@angular/core'
);
if (!angularCoreImport.getNamedImports().find(el => el.getText() === 'OnDestroy')) {
angularCoreImport.addNamedImport('OnDestroy');
}

const rxjsImport = sourceFile.getImportDeclaration(imp => imp.getModuleSpecifierValue() === 'rxjs');
if (!rxjsImport) {
sourceFile.addImportDeclaration({
namedImports: ['Subject'],
moduleSpecifier: 'rxjs',
});
} else if (!rxjsImport.getNamedImports().find(el => el.getText() === 'Subject')) {
rxjsImport.addNamedImport('Subject');
}
sourceFile.formatText({ indentSize: 2, convertTabsToSpaces: true });
host.overwrite(path, sourceFile.getText());

const operations = [];
if (!options.ci) {
operations.push(applyLintFix());
}

return chain(operations);
};
}
110 changes: 110 additions & 0 deletions schematics/src/add-destroy-subject-to-component/factory_spec.ts
@@ -0,0 +1,110 @@
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { switchMap } from 'rxjs/operators';

import { PWAComponentOptionsSchema } from '../component/schema';
import { createApplication, createSchematicRunner } from '../utils/testHelper';

describe('Lazy Component Schematic', () => {
const schematicRunner = createSchematicRunner();

let appTree: UnitTestTree;
beforeEach(async () => {
appTree = await createApplication(schematicRunner)
.pipe(
switchMap(tree =>
schematicRunner.runSchematicAsync(
'component',
{
project: 'bar',
name: 'foo',
} as PWAComponentOptionsSchema,
tree
)
)
)
.toPromise();
});

it('should be created', () => {
expect(appTree.files).toContain('/src/app/foo/foo.component.ts');
});

it('should be runnable on a component', async () => {
await schematicRunner
.runSchematicAsync(
'add-destroy-subject-to-component',
{
project: 'bar',
name: 'src/app/foo/foo.component.ts',
},
appTree
)
.toPromise();
});

it('should be runnable on the folder of a component', async () => {
await schematicRunner
.runSchematicAsync(
'add-destroy',
{
project: 'bar',
name: 'src/app/foo',
},
appTree
)
.toPromise();
});

it('should be runnable on the relative project folder of a component', async () => {
await schematicRunner
.runSchematicAsync(
'add-destroy',
{
project: 'bar',
name: 'foo',
},
appTree
)
.toPromise();
});

describe('after run', () => {
let content: string;

beforeEach(async () => {
appTree = await schematicRunner
.runSchematicAsync(
'add-destroy-subject-to-component',
{
project: 'bar',
name: 'src/app/foo/foo.component.ts',
},
appTree
)
.toPromise();
content = appTree.readContent('src/app/foo/foo.component.ts');
});

it('should add import for OnDestroy', () => {
expect(content).toMatch(/OnDestroy.* from .@angular\/core.;/);
});

it('should add import for Subject', () => {
expect(content).toMatch(/Subject.* from .rxjs.;/);
});

it('should add implements for OnDestroy', () => {
expect(content).toMatch(/implements.*OnDestroy/);
});

it('should add destroy$ subject', () => {
expect(content).toContain('destroy$ = new Subject();');
});

it('should add ngOnDestroy method', () => {
expect(content).toContain('ngOnDestroy()');
expect(content).toContain('this.destroy$.next();');
expect(content).toContain('this.destroy$.complete();');
});
});
});
25 changes: 25 additions & 0 deletions schematics/src/add-destroy-subject-to-component/schema.json
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsPWAComponent",
"title": "PWA Add Destroy Subject To Component Options Schema",
"type": "object",
"description": "Adds destroy subject to an existing Angular artifact.",
"properties": {
"project": {
"type": "string",
"$default": {
"$source": "projectName"
},
"visible": false
},
"name": {
"type": "string",
"description": "The path of the component.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What component should the destroy be added to?"
}
}
}
5 changes: 5 additions & 0 deletions schematics/src/collection.json
Expand Up @@ -96,6 +96,11 @@
"factory": "./customized-copy/factory#customize",
"description": "Create a copy of Component for customization.",
"schema": "./customized-copy/schema.json"
},
"add-destroy-subject-to-component": {
"factory": "./add-destroy-subject-to-component/factory#add",
"description": "Add destroy subject to an existing Angular artifact.",
"schema": "./add-destroy-subject-to-component/schema.json"
}
}
}
15 changes: 15 additions & 0 deletions schematics/src/utils/ts-morph.ts
@@ -0,0 +1,15 @@
import { Tree } from '@angular-devkit/schematics';
import { existsSync, readFileSync } from 'fs';
import { FileSystemHost, Project } from 'ts-morph';

export function createTsMorphProject(host: Tree) {
return new Project({
// tslint:disable-next-line: ish-no-object-literal-type-assertion
fileSystem: {
getCurrentDirectory: () => '',
directoryExistsSync: p => host.exists(p) || existsSync(p),
fileExistsSync: p => host.exists(p) || existsSync(p),
readFileSync: (p, encoding) => (host.read(p) || readFileSync(p)).toString(encoding),
} as FileSystemHost,
});
}

0 comments on commit 4013ff6

Please sign in to comment.