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

Angular Schematics

Maciej Treder
November 09, 2019

Angular Schematics

Maciej Treder

November 09, 2019
Tweet

More Decks by Maciej Treder

Other Decks in Technology

Transcript

  1. @maciejtreder

    View Slide

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

    Akamai Technologies
    • Angular passionate
    • Open source contributor (founder of @ng-toolkit project)
    • Articles author

    View Slide

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

    View Slide

  4. Angular Schematics
    Develop for developers

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. I want that library!
    Case Study

    View Slide

  11. Follow the guidance!
    Case Study

    View Slide

  12. Case Study

    View Slide

  13. 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

    View Slide

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

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

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

    View Slide

  20. 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

    View Slide

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

    View Slide

  22. 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

    View Slide

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

    View Slide

  24. I want that library!
    Schematics

    View Slide

  25. 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

    View Slide

  26. Enhance your library
    Schematics

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. 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

    View Slide

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

    View Slide

  37. 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

    View Slide

  38. Follow the guidance!
    Schematics

    View Slide

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

    View Slide

  40. 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

    View Slide

  41. {
    "$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

    View Slide

  42. //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

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. Reporting

    View Slide

  46. 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

    View Slide

  47. 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

    View Slide

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

    View Slide

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

    View Slide

  50. Feedback
    http://bit.ly/32vxs5F

    View Slide

  51. @maciejtreder

    View Slide