Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Angular Schematics

Avatar for Maciej Treder Maciej Treder
November 09, 2019

Angular Schematics

Avatar for Maciej Treder

Maciej Treder

November 09, 2019
Tweet

More Decks by Maciej Treder

Other Decks in Technology

Transcript

  1. • Kraków, Poland • Senior Software Development Engineer in Test

    
 Akamai Technologies • Angular passionate • Open source contributor (founder of @ng-toolkit project) • Articles author
  2. • Cambridge, Massachusetts • Content Delivery Network • Over 240

    00 servers deployed in more then 120 countries • Serves 15%-30% of the Internet traffic
  3. Outline • My own story • Case studies • Schematics

    • Reporting • Testing Story Case StudySchematics Reporting Testing
  4. Innovation Begins With an Idea • SEO friendly • Works

    offline • Cheap environment - Angular Universal - PWA - Serverless Story
  5. Do Not Reinvent the Wheel • Angular Webpack starter (https://github.com/preboot/angular-webpack)

    • Angular Universal starter (https://github.com/angular/universal-starter) Story
  6. Yet Another Boilerplate… • Progressive Web App • Server-Side Rendering

    • Hosted on AWS Lambda • Uploaded to GitHub • ~30 clones weekly angular-universal-serverless-pwa Story
  7. 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
  8. ng add @ng-toolkit/universal • Install the dependency • Look up

    for schematics • Apply changes to the filesystem Schematics
  9. package.json { "author": "Maciej Treder <[email protected]>", "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
  10. 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
  11. 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
  12. 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
  13. schematics.ts export default function index(options: any): Rule { if (options.http)

    { //true or nothing was passed (default value) } else { //false was passed } } Schematics
  14. Rule • Set of instructions for the Angular CLI •

    (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | 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
  15. Tree • Object which represents file system • Supports CRUD

    operations and more: • exists() • getDir() • visit() • etc Schematics
  16. 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
  17. tree Chaining export default function index(options: any): Rule { return

    chain([ rule1, rule2, rule3 ]) } rule1 rule2 rule3 Schematics
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. { "$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
  31. //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
  32. Tree | Observable<Tree> | 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
  33. import * as bugsnag from 'bugsnag'; export function applyAndLog(rule: Rule):

    Rule { bugsnag.register(‘PROJECT_KEY’); return (tree: Tree, context: SchematicContext) => { } } return (<Observable<Tree>> rule(tree, context)) .pipe(catchError((error: any) => { })); let subject: Subject<Tree> = 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
  34. 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
  35. 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
  36. e2e Testing npm install -g verdaccio verdaccio --config default.yaml >>

    verdacio_output & npm set registry=http://localhost:4873/ echo "//localhost:4873/:_authToken=\"CjmKyL6UDkX6FDpNnP64fw==\"" >> ~/.npmrc