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