Share
Embed
Transcript
51 slides
-
• Kraków, Poland • Senior Software Development Engineer in Test
Akamai Technologies • Angular passionate • Open source contributor (founder of @ng-toolkit project) • Articles author -
• Cambridge, Massachusetts • Content Delivery Network • Over 240
00 servers deployed in more then 120 countries • Serves 15%-30% of the Internet traffic -
Outline • My own story • Case studies • Schematics
• Reporting • Testing Story Case StudySchematics Reporting Testing -
Innovation Begins With an Idea • SEO friendly • Works
offline • Cheap environment - Angular Universal - PWA - Serverless Story -
Do Not Reinvent the Wheel • Angular Webpack starter (https://github.com/preboot/angular-webpack)
• Angular Universal starter (https://github.com/angular/universal-starter) Story -
Yet Another Boilerplate… • Progressive Web App • Server-Side Rendering
• Hosted on AWS Lambda • Uploaded to GitHub • ~30 clones weekly angular-universal-serverless-pwa Story -
The Gray Eminence Is GIT designed for hosting dev tools?
@esosanderelias Story -
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 -
ng add @ng-toolkit/universal • Install the dependency • Look up
for schematics • Apply changes to the filesystem Schematics -
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 -
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 -
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 -
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 -
schematics.ts export default function index(options: any): Rule { if (options.http)
{ //true or nothing was passed (default value) } else { //false was passed } } Schematics -
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 -
Tree • Object which represents file system • Supports CRUD
operations and more: • exists() • getDir() • visit() • etc Schematics -
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 -
tree Chaining export default function index(options: any): Rule { return
chain([ rule1, rule2, rule3 ]) } rule1 rule2 rule3 Schematics -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
is… • isImportDeclaration • isVariableDeclaration • isClassDeclaration • isMethodDeclaration •
isStringLiteral • isIfStatement Schematics -
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 -
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 -
{ "$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 -
//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 -
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 -
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 -
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 -
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 -
e2e Testing npm install -g verdaccio verdaccio --config default.yaml >>
verdacio_output & npm set registry=http://localhost:4873/ echo "//localhost:4873/:_authToken=\"CjmKyL6UDkX6FDpNnP64fw==\"" >> ~/.npmrc