Slide 1

Slide 1 text

@maciejtreder

Slide 2

Slide 2 text

• Kraków, Poland • Senior Software Development Engineer in Test 
 Akamai Technologies • Angular passionate • Open source contributor (founder of @ng-toolkit project) • Articles author

Slide 3

Slide 3 text

• Cambridge, Massachusetts • Content Delivery Network • Over 240 00 servers deployed in more then 120 countries • Serves 15%-30% of the Internet traffic

Slide 4

Slide 4 text

Angular Schematics Develop for developers

Slide 5

Slide 5 text

Outline • My own story • Case studies • Schematics • Reporting • Testing Story Case StudySchematics Reporting Testing

Slide 6

Slide 6 text

Innovation Begins With an Idea • SEO friendly • Works offline • Cheap environment - Angular Universal - PWA - Serverless Story

Slide 7

Slide 7 text

Do Not Reinvent the Wheel • Angular Webpack starter (https://github.com/preboot/angular-webpack) • Angular Universal starter (https://github.com/angular/universal-starter) Story

Slide 8

Slide 8 text

Yet Another Boilerplate… • Progressive Web App • Server-Side Rendering • Hosted on AWS Lambda • Uploaded to GitHub • ~30 clones weekly angular-universal-serverless-pwa Story

Slide 9

Slide 9 text

The Gray Eminence Is GIT designed for hosting dev tools? @esosanderelias Story

Slide 10

Slide 10 text

I want that library! Case Study

Slide 11

Slide 11 text

Follow the guidance! Case Study

Slide 12

Slide 12 text

Case Study

Slide 13

Slide 13 text

Schematics • Set of instructions (rules) consumed by the Angular CLI to manipulate the file- system and perform NodeJS tasks • Extensible - possible to combine multiple internal and external rules • Atomic - “commit approach”/“all or nothing” ng add/update/init/something Schematics

Slide 14

Slide 14 text

ng add @ng-toolkit/universal • Install the dependency • Look up for schematics • Apply changes to the filesystem Schematics

Slide 15

Slide 15 text

package.json { "author": "Maciej Treder ", "name": "@ng-toolkit/universal", "main": "dist/index.js", "version": "1.1.50", "description": "Adds Angular Universal support for any Angular CLI project", "repository": { "type": "git", "url": "git+https://github.com/maciejtreder/ng-toolkit.git" }, "license": "MIT", "schematics": "./collection.json", "peerDependencies": { }, "dependencies": { }, "devDependencies": { }, "publishConfig": { "access": "public" } } "schematics": "./collection.json", Schematics

Slide 16

Slide 16 text

collection.json { "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { "factory": "./schematics", "description": "Update an application with server side rendering (Angular Universal)", "schema": "./schema.json" } } } "factory": "./schematics", "schema": "./schema.json" Schematics

Slide 17

Slide 17 text

schema.json { "$schema": "http://json-schema.org/schema", "id": "ng-toolkit universal", "title": "Angular Application Options Schema”, "type": "object", "required": [] } ng add @ng-toolkit/universal —http false "properties": { "directory": { "description": "App root catalog", "type": "string", "default": "." }, "http": { "description": "Determines if you want to install TransferHttpCacheModule", "type": "boolean", "default": true } }, Schematics

Slide 18

Slide 18 text

Prompts { "$schema": "http://json-schema.org/schema", "id": "ng-toolkit universal", "title": "Angular Application Options Schema”, "type": “object", "required": [] } "properties": { "http": { “x-prompt": “What’s your name?”, "type": “string", "default": “John" } }, Schematics

Slide 19

Slide 19 text

schematics.ts export default function index(options: any): Rule { if (options.http) { //true or nothing was passed (default value) } else { //false was passed } } Schematics

Slide 20

Slide 20 text

Rule • Set of instructions for the Angular CLI • (tree: Tree, context: SchematicContext) => Tree | Observable | Rule | void let rule: Rule = (tree: Tree) => { tree.create('hello', 'world'); return tree; } krk-mps4m:js-fest mtreder$ ls hello krk-mps4m:js-fest mtreder$ cat hello world CLI Schematics

Slide 21

Slide 21 text

Tree • Object which represents file system • Supports CRUD operations and more: • exists() • getDir() • visit() • etc Schematics

Slide 22

Slide 22 text

tree. tree.create('path', 'content'); tree.exists('path') tree.overwrite('path', 'new file content'); tree.getDir(`${options.directory}/src/environments`).visit( (path: Path) => { if (path.endsWith('.ts')) { addEntryToEnvironment(tree, path, 'line to be inserted'); } }); const recorder = tree.beginUpdate(‘filePath’); recorder.insertRight(0, ‘console.log(\'Hello World!\’);') tree.commitUpdate(recorder); Schematics

Slide 23

Slide 23 text

tree Chaining export default function index(options: any): Rule { return chain([ rule1, rule2, rule3 ]) } rule1 rule2 rule3 Schematics

Slide 24

Slide 24 text

I want that library! Schematics

Slide 25

Slide 25 text

Enhance your library • Create schematics/ folder inside your project • Within schematics/ create ng-add/ for your first schematics • Place collection.json inside schematics/ • Prepare tsconfig.json for schematics • Use schematics property in the package.json to point to the collection.json • Compile and publish schematics together your library Schematics

Slide 26

Slide 26 text

Enhance your library Schematics

Slide 27

Slide 27 text

package.json "scripts": { "build": "ng build --prod && tsc -p tsconfig.json && npm run copy_files", "copy_files": "cp-cli schematics dist/schematics", "test": "npm run build && jasmine src/**/*_spec.js", "prepublish": "npm test", "ci-publish": "ci-publish" }, "schematics": "./collection.json", Schematics

Slide 28

Slide 28 text

collection.json { "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { "factory": “./ng-add", "description": "Update an application with server side rendering (Angular Universal)", "schema": “./schema.json" } } } Schematics

Slide 29

Slide 29 text

schema.json { "$schema": "http://json-schema.org/schema", "id": "ng-toolkit universal", "title": "Angular Application Options Schema”, "type": "object", "required": [] } "properties": { "directory": { "description": "App root catalog", "type": "string", "default": "." }, "http": { "description": "Determines if you want to install TransferHttpCacheModule", "type": "boolean", "default": true } }, Schematics

Slide 30

Slide 30 text

schematics.ts import { apply, chain, mergeWith, move, Rule, url, MergeStrategy } from '@angular-devkit/schematics'; import { IToolkitUniversalSchema } from './schema'; export default function addUniversal(options: IToolkitUniversalSchema): Rule { const rules: Rule[] = []; return chain(rules); } const templateSource = apply(url('./files'), [move(options.directory)]); rules.push(mergeWith(templateSource, MergeStrategy.Overwrite)); rules.push(createHelloWorld(options)); function createHelloWorld(options: IToolkitUniversalSchema): Rule { return tree => { tree.create( `${options.appDir}/hello.ts`, 'console.log("world");'); return tree; } } Schematics

Slide 31

Slide 31 text

ng update { "ng-update": { "requirements": { "my-lib": "^5" }, "migrations": "./migrations/migration-collection.json" } } { "schematics": { "migration-01": { "version": "6", "factory": "./update-6" }, "migration-02": { "version": "6.2", "factory": "./update-6_2" }, "migration-03": { "version": "6.3", "factory": "./update-6_3" } } } Updates one or multiple packages, its peer dependencies and the peer dependencies that depends on them. package.json Schematics

Slide 32

Slide 32 text

ng add & update export default function addUniversal(options: any): Rule { const rules: Rule[] = []; rules.push(initial(options)); rules.push(update1()); rules.push(update2()); return chain(rules); } Schematics

Slide 33

Slide 33 text

Task:
 change all ‘window’ occurences to ‘this.window’ Solution: export default function replaceWindow(options: IToolkitUniversalSchema): Rule { return tree => { let code = getFileContent(tree, 'some.component.ts'); code = code.replace(/window/g, "this.window"); tree.overwrite('some.compoennt.ts', code); return tree; } } Working with source-code Schematics

Slide 34

Slide 34 text

Working with source-code export class MyClass { private message = 'Do not open window!'; console.log(otherwindow); } export class MyClass { private message = 'Do not open this.window!’; console.log(otherthis.window); } Schematics

Slide 35

Slide 35 text

Working with source-code import { Rule, SchematicsException, Tree, SchematicContext } from ‘@angular-devkit/schematics'; import { getFileContent } from '@schematics/angular/utility/test'; import * as ts from 'typescript'; export default function(options: any): Rule { return (tree: Tree, context: SchematicContext) => { return tree; } } const filePath = `${options.directory}/sourceFile.ts`; const recorder = tree.beginUpdate(filePath); let fileContent = getFileContent(tree, filePath); let sourceFile: ts.SourceFile = ts.createSourceFile('temp.ts', fileContent, ts.ScriptTarget.Latest) sourceFile.forEachChild(node => { }); if (ts.isClassDeclaration(node)) { node.members.forEach(node => { if (ts.isConstructorDeclaration(node)) { if (node.body) { } } }); } recorder.insertRight(node.body.pos + 1, 'console.log(\'constructor!\');') tree.commitUpdate(recorder); Schematics

Slide 36

Slide 36 text

is… • isImportDeclaration • isVariableDeclaration • isClassDeclaration • isMethodDeclaration • isStringLiteral • isIfStatement Schematics

Slide 37

Slide 37 text

SchematicContext export default function(options: any): Rule { return (tree: Tree, context: SchematicContext) => { context.addTask(new NodePackageInstallTask(options.directory)); return tree; } } • TslintFixTask • RunSchematicTask • NodePackageInstallTask • NodePackageLinkTask • RepositoryInitializerTask Schematics

Slide 38

Slide 38 text

Follow the guidance! Schematics

Slide 39

Slide 39 text

Yet another CLI npm install -g @angular-devkit/schematics-cli schematics blank hello Schematics

Slide 40

Slide 40 text

import { Rule, SchematicContext, Tree, chain, externalSchematic } from '@angular-devkit/schematics'; export function myComponent(options: any): Rule { const licenseText = "//Hello from schematics!\n"; } return chain([ externalSchematic('@schematics/angular', 'component', options), (tree: Tree, _context: SchematicContext) => { } ]); tree.getDir(`src/app/${options.name}`) .visit(filePath => { }); return tree; // Prevent from writing license to files that already have one. if (content.indexOf(licenseText) == -1) { tree.overwrite(filePath, licenseText + content); } if (!filePath.endsWith('.ts')) { return; } const content = tree.read(filePath); if (!content) { return; } Schematics

Slide 41

Slide 41 text

{ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "my-component": { "description": "A blank schematic.", "factory": "./my-component/index#myComponent", "schema": "../node_modules/@schematics/angular/component/schema.json" } } } collection.json Schematics

Slide 42

Slide 42 text

//Hello from schematics! import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-hello-world', templateUrl: './hello-world.component.html', styleUrls: ['./hello-world.component.css'] }) export class HelloWorldComponent implements OnInit { constructor() { } ngOnInit() { } } ng generate my-component:my-component HelloWorld Schematics

Slide 43

Slide 43 text

Tree | Observable | Rule | void export function performAdditionalAction(originalRule: Rule): Rule { return (tree: Tree, context: SchematicContext) => { originalRule.apply(tree, context) .pipe(map( (tree: Tree) => console.log(tree.exists('hello')) ) ); } } Reporting

Slide 44

Slide 44 text

import * as bugsnag from 'bugsnag'; export function applyAndLog(rule: Rule): Rule { bugsnag.register(‘PROJECT_KEY’); return (tree: Tree, context: SchematicContext) => { } } return (> rule(tree, context)) .pipe(catchError((error: any) => { })); let subject: Subject = new Subject(); bugsnag.notify(error, (bugsnagError, response) => { }); return subject; if (!bugsnagError && response === 'OK') { console.log(`Stacktrace sent to tracking system.`); } subject.next(Tree.empty()); subject.complete(); Reporting

Slide 45

Slide 45 text

Reporting

Slide 46

Slide 46 text

Testing import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; const collectionPath = path.join(__dirname, './collection.json'); describe('Universal', () => { let appTree: UnitTestTree; const schematicRunner = new SchematicTestRunner('@ng-toolkit/universal', collectionPath); const appOptions: any = { name: 'foo', version: '7.0.0'}; }); beforeEach((done) => { appTree = new UnitTestTree(Tree.empty()); }); schematicRunner.runExternalSchematicAsync( '@schematics/angular', 'ng-new', appOptions, appTree ).subscribe(tree => { appTree = tree done(); }); Testing

Slide 47

Slide 47 text

Testing const defaultOptions: any = { project: 'foo', disableBugsnag: true, directory: '/foo' }; it('Should add server build', (done) => { schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).subscribe(tree => { const cliConfig = JSON.parse(getFileContent(tree, `${defaultOptions.directory}/angular.json`)); expect(cliConfig.projects.foo.architect.server).toBeDefined(`Can't find server build`); done(); }); }) Testing

Slide 48

Slide 48 text

e2e Testing npm install -g verdaccio verdaccio --config default.yaml >> verdacio_output & npm set registry=http://localhost:4873/ echo "//localhost:4873/:_authToken=\"CjmKyL6UDkX6FDpNnP64fw==\"" >> ~/.npmrc

Slide 49

Slide 49 text

e2e cd mySchematics npm publish cd ../testProject ng add mySchematics Testing

Slide 50

Slide 50 text

Feedback http://bit.ly/32vxs5F

Slide 51

Slide 51 text

@maciejtreder