From b00464a0f6e3971418e9d4fbffa0a6b077ada4e4 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 30 Sep 2016 14:22:16 -0700 Subject: [PATCH] feat(@ngtools/logger): Implement a reactive logger. It is typescript friendly and ultra performant. Using it in e2e tests for a PoC. --- .appveyor.yml | 2 +- .travis.yml | 12 +- package.json | 9 +- packages/@angular-cli/ast-tools/tsconfig.json | 5 +- packages/@ngtools/logger/package.json | 10 ++ .../logger/src/console-logger-stack.spec.ts | 78 +++++++++++ .../logger/src/console-logger-stack.ts | 105 ++++++++++++++ packages/@ngtools/logger/src/indent.spec.ts | 33 +++++ packages/@ngtools/logger/src/indent.ts | 37 +++++ packages/@ngtools/logger/src/index.ts | 6 + packages/@ngtools/logger/src/logger.spec.ts | 46 +++++++ packages/@ngtools/logger/src/logger.ts | 116 ++++++++++++++++ .../@ngtools/logger/src/null-logger.spec.ts | 37 +++++ packages/@ngtools/logger/src/null-logger.ts | 13 ++ .../logger/src/transform-logger.spec.ts | 32 +++++ .../@ngtools/logger/src/transform-logger.ts | 13 ++ packages/@ngtools/logger/tsconfig.json | 25 ++++ scripts/publish/build.js | 24 +++- scripts/run-packages-spec.js | 2 - tests/e2e/utils/process.ts | 12 +- tests/{e2e_runner.js => e2e_runner.ts} | 129 ++++++++++-------- tests/run_e2e.js | 4 + 22 files changed, 668 insertions(+), 82 deletions(-) create mode 100644 packages/@ngtools/logger/package.json create mode 100644 packages/@ngtools/logger/src/console-logger-stack.spec.ts create mode 100644 packages/@ngtools/logger/src/console-logger-stack.ts create mode 100644 packages/@ngtools/logger/src/indent.spec.ts create mode 100644 packages/@ngtools/logger/src/indent.ts create mode 100644 packages/@ngtools/logger/src/index.ts create mode 100644 packages/@ngtools/logger/src/logger.spec.ts create mode 100644 packages/@ngtools/logger/src/logger.ts create mode 100644 packages/@ngtools/logger/src/null-logger.spec.ts create mode 100644 packages/@ngtools/logger/src/null-logger.ts create mode 100644 packages/@ngtools/logger/src/transform-logger.spec.ts create mode 100644 packages/@ngtools/logger/src/transform-logger.ts create mode 100644 packages/@ngtools/logger/tsconfig.json rename tests/{e2e_runner.js => e2e_runner.ts} (62%) create mode 100644 tests/run_e2e.js diff --git a/.appveyor.yml b/.appveyor.yml index c2e095f9be51..2a2d9297d5ce 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,6 +14,6 @@ test_script: - node --version - npm --version - npm test - - node tests\e2e_runner.js + - node tests\run_e2e.js build: off diff --git a/.travis.yml b/.travis.yml index f09e83ca2e5c..8e551b7278b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: allow_failures: - os: osx - node_js: "7" - - env: NODE_SCRIPT="tests/e2e_runner.js --nightly" + - env: NODE_SCRIPT="tests/run_e2e.js --nightly" include: - node_js: "6" os: linux @@ -22,16 +22,16 @@ matrix: env: SCRIPT=build - node_js: "4" os: linux - env: NODE_SCRIPT=tests/e2e_runner.js + env: NODE_SCRIPT=tests/run_e2e.js - node_js: "6" os: linux env: SCRIPT=test - node_js: "6" os: linux - env: NODE_SCRIPT=tests/e2e_runner.js + env: NODE_SCRIPT=tests/run_e2e.js - node_js: "6" os: osx - env: NODE_SCRIPT=tests/e2e_runner.js + env: NODE_SCRIPT=tests/run_e2e.js # Optional builds. - node_js: "6" @@ -39,10 +39,10 @@ matrix: env: SCRIPT=test - node_js: "6" os: linux - env: NODE_SCRIPT="tests/e2e_runner.js --nightly" + env: NODE_SCRIPT="tests/run_e2e.js --nightly" - node_js: "7" os: linux - env: NODE_SCRIPT=tests/e2e_runner.js + env: NODE_SCRIPT=tests/run_e2e.js before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi diff --git a/package.json b/package.json index 17cda0eb34e8..ba2f95d6c3d1 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "@types/glob": "^5.0.29", "@types/jasmine": "^2.2.32", "@types/lodash": "^4.14.43", - "@types/mock-fs": "3.6.28", + "@types/mock-fs": "^3.6.30", "@types/node": "^6.0.36", "@types/request": "0.0.30", "@types/rimraf": "0.0.25-alpha", @@ -151,11 +151,9 @@ "conventional-changelog": "^1.1.0", "dtsgenerator": "^0.7.1", "eslint": "^2.8.0", - "exists-sync": "0.0.3", "express": "^4.14.0", "jasmine": "^2.4.1", "jasmine-spec-reporter": "^2.7.0", - "minimatch": "^3.0.3", "minimist": "^1.2.0", "mocha": "^2.4.5", "mock-fs": "3.10.0", @@ -166,10 +164,7 @@ "resolve-bin": "^0.4.0", "rewire": "^2.5.1", "sinon": "^1.17.3", - "through": "^2.3.8", "tree-kill": "^1.0.0", - "ts-node": "^1.3.0", - "tslint": "^4.0.2", - "walk-sync": "^0.2.6" + "ts-node": "^1.3.0" } } diff --git a/packages/@angular-cli/ast-tools/tsconfig.json b/packages/@angular-cli/ast-tools/tsconfig.json index 584a9436cc7b..a6c7a38c8bc8 100644 --- a/packages/@angular-cli/ast-tools/tsconfig.json +++ b/packages/@angular-cli/ast-tools/tsconfig.json @@ -20,8 +20,5 @@ "jasmine", "node" ] - }, - "exclude": [ - "**/*.spec.ts" - ] + } } diff --git a/packages/@ngtools/logger/package.json b/packages/@ngtools/logger/package.json new file mode 100644 index 000000000000..2d8bc2ca1cc5 --- /dev/null +++ b/packages/@ngtools/logger/package.json @@ -0,0 +1,10 @@ +{ + "name": "@ngtools/logger", + "version": "0.1.0", + "description": "", + "main": "./src/index.js", + "license": "MIT", + "peerDependencies": { + "rxjs": "^5.0.1" + } +} diff --git a/packages/@ngtools/logger/src/console-logger-stack.spec.ts b/packages/@ngtools/logger/src/console-logger-stack.spec.ts new file mode 100644 index 000000000000..0cc99be052d1 --- /dev/null +++ b/packages/@ngtools/logger/src/console-logger-stack.spec.ts @@ -0,0 +1,78 @@ +import {LogEntry, Logger} from './logger'; +import {ConsoleLoggerStack} from './console-logger-stack'; +import {NullLogger} from './null-logger'; + + +describe('ConsoleLoggerStack', () => { + it('works', (done: DoneFn) => { + const logger = ConsoleLoggerStack.start('test'); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'hello', level: 'debug', name: 'test' }), + jasmine.objectContaining({ message: 'world', level: 'info', name: 'test' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + console.debug('hello'); + console.log('world'); + ConsoleLoggerStack.end(); + }); + + it('works as a stack', (done: DoneFn) => { + const oldConsoleLog = console.log; + const logger = ConsoleLoggerStack.start('test'); + expect(console.log).not.toBe(oldConsoleLog); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'red', level: 'info', name: 'test' }), + jasmine.objectContaining({ message: 'blue', level: 'info', name: 'test2' }), + jasmine.objectContaining({ message: 'yellow', level: 'info', name: 'test3' }), + jasmine.objectContaining({ message: 'green', level: 'info', name: 'test2' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + console.log('red'); + ConsoleLoggerStack.push('test2'); + console.log('blue'); + ConsoleLoggerStack.push('test3'); + console.log('yellow'); + ConsoleLoggerStack.pop(); + console.log('green'); + ConsoleLoggerStack.end(); + expect(console.log).toBe(oldConsoleLog); + }); + + it('can push instances or classes', (done: DoneFn) => { + const oldConsoleLog = console.log; + const logger = new Logger('test'); + ConsoleLoggerStack.start(logger); + expect(console.log).not.toBe(oldConsoleLog); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'red', level: 'info', name: 'test' }), + jasmine.objectContaining({ message: 'green', level: 'info', name: 'test2' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + console.log('red'); + ConsoleLoggerStack.push(new NullLogger(logger)); + console.log('blue'); + ConsoleLoggerStack.pop(); + ConsoleLoggerStack.push(new Logger('test2', logger)); + console.log('green'); + ConsoleLoggerStack.end(); + expect(console.log).toBe(oldConsoleLog); + }); +}); diff --git a/packages/@ngtools/logger/src/console-logger-stack.ts b/packages/@ngtools/logger/src/console-logger-stack.ts new file mode 100644 index 000000000000..e8271fdea5bb --- /dev/null +++ b/packages/@ngtools/logger/src/console-logger-stack.ts @@ -0,0 +1,105 @@ +import {Logger} from './logger'; + +let globalConsoleStack: Logger[] = null; +let originalConsoleDebug: (message?: any, ...optionalParams: any[]) => void; +let originalConsoleLog: (message?: any, ...optionalParams: any[]) => void; +let originalConsoleWarn: (message?: any, ...optionalParams: any[]) => void; +let originalConsoleError: (message?: any, ...optionalParams: any[]) => void; + + +function _push(logger: Logger) { + if (globalConsoleStack.length == 0) { + originalConsoleDebug = console.debug; + originalConsoleLog = console.log; + originalConsoleWarn = console.warn; + originalConsoleError = console.error; + + console.debug = (msg: string, ...args: any[]) => { + globalConsoleStack[globalConsoleStack.length - 1].debug(msg, { args }); + }; + console.log = (msg: string, ...args: any[]) => { + globalConsoleStack[globalConsoleStack.length - 1].info(msg, { args }); + }; + console.warn = (msg: string, ...args: any[]) => { + globalConsoleStack[globalConsoleStack.length - 1].warn(msg, { args }); + }; + console.error = (msg: string, ...args: any[]) => { + globalConsoleStack[globalConsoleStack.length - 1].error(msg, { args }); + }; + } + globalConsoleStack.push(logger); + + return logger; +} + +function _pop() { + globalConsoleStack[globalConsoleStack.length - 1].complete(); + globalConsoleStack.pop(); + if (globalConsoleStack.length == 0) { + console.log = originalConsoleLog; + console.warn = originalConsoleWarn; + console.error = originalConsoleError; + console.debug = originalConsoleDebug; + globalConsoleStack = null; + } +} + + +export type LoggerConstructor = { + new (...args: any[]): T; +}; + + +export class ConsoleLoggerStack { + static push(name: string): Logger; + static push(logger: Logger): Logger; + static push(loggerClass: LoggerConstructor, ...args: any[]): Logger; + static push(nameOrLogger: string | Logger | LoggerConstructor = '', + ...args: any[]) { + if (typeof nameOrLogger == 'string') { + return _push(new Logger(nameOrLogger as string, this.top())); + } else if (nameOrLogger instanceof Logger) { + const logger = nameOrLogger as Logger; + if (logger.parent !== this.top()) { + throw new Error('Pushing a logger that is not a direct child of the top of the stack.'); + } + return _push(logger); + } else { + const klass = nameOrLogger as LoggerConstructor; + return _push(new klass(...args, this.top())); + } + } + static pop(): Logger | null { + _pop(); + return this.top(); + } + + static top(): Logger | null { + return globalConsoleStack && globalConsoleStack[globalConsoleStack.length - 1]; + } + + static start(name: string): Logger; + static start(logger: Logger): Logger; + static start(loggerClass: LoggerConstructor, ...args: any[]): Logger; + static start(nameOrLogger: string | Logger | LoggerConstructor = '', + ...args: any[]) { + if (globalConsoleStack !== null) { + throw new Error('Cannot start a new console logger stack while one is already going.'); + } + + globalConsoleStack = []; + if (typeof nameOrLogger == 'string') { + return _push(new Logger(nameOrLogger as string, this.top())); + } else if (nameOrLogger instanceof Logger) { + return _push(nameOrLogger as Logger); + } else { + const klass = nameOrLogger as LoggerConstructor; + return _push(new klass(...args, this.top())); + } + } + static end() { + while (globalConsoleStack !== null) { + this.pop(); + } + } +} diff --git a/packages/@ngtools/logger/src/indent.spec.ts b/packages/@ngtools/logger/src/indent.spec.ts new file mode 100644 index 000000000000..aae7a897c6eb --- /dev/null +++ b/packages/@ngtools/logger/src/indent.spec.ts @@ -0,0 +1,33 @@ +import {LogEntry, Logger} from './logger'; +import {IndentLogger} from './indent'; + + +describe('IndentSpec', () => { + it('works', (done: DoneFn) => { + const logger = new IndentLogger('test'); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'test', level: 'info', name: 'test' }), + jasmine.objectContaining({ message: ' test2', level: 'info', name: 'test2' }), + jasmine.objectContaining({ message: ' test3', level: 'info', name: 'test3' }), + jasmine.objectContaining({ message: ' test4', level: 'info', name: 'test4' }), + jasmine.objectContaining({ message: 'test5', level: 'info', name: 'test' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + const logger2 = new Logger('test2', logger); + const logger3 = new Logger('test3', logger2); + const logger4 = new Logger('test4', logger); + + logger.info('test'); + logger2.info('test2'); + logger3.info('test3'); + logger4.info('test4'); + logger.info('test5'); + + logger.complete(); + }); +}); diff --git a/packages/@ngtools/logger/src/indent.ts b/packages/@ngtools/logger/src/indent.ts new file mode 100644 index 000000000000..98cdc21f77cb --- /dev/null +++ b/packages/@ngtools/logger/src/indent.ts @@ -0,0 +1,37 @@ +import {Logger} from './logger'; + +import 'rxjs/add/operator/map'; + + +/** + * Keep an map of indentation => array of indentations based on the level. + * This is to optimize calculating the prefix based on the indentation itself. Since most logs + * come from similar levels, and with similar indentation strings, this will be shared by all + * loggers. Also, string concatenation is expensive so performing concats for every log entries + * is expensive; this alleviates it. + */ +const indentationMap: {[indentationType: string]: string[]} = {}; + + +export class IndentLogger extends Logger { + constructor(name: string, parent: Logger | null = null, indentation = ' ') { + super(name, parent); + + indentationMap[indentation] = indentationMap[indentation] || ['']; + const map = indentationMap[indentation]; + + this._observable = this._observable.map(entry => { + const l = entry.path.length; + if (l >= map.length) { + let current = map[map.length - 1]; + while (l >= map.length) { + current += indentation; + map.push(current); + } + } + + entry.message = map[l] + entry.message; + return entry; + }); + } +} diff --git a/packages/@ngtools/logger/src/index.ts b/packages/@ngtools/logger/src/index.ts new file mode 100644 index 000000000000..2c16a440a527 --- /dev/null +++ b/packages/@ngtools/logger/src/index.ts @@ -0,0 +1,6 @@ + +export * from './console-logger-stack'; +export * from './indent'; +export * from './logger'; +export * from './null-logger'; +export * from './transform-logger'; diff --git a/packages/@ngtools/logger/src/logger.spec.ts b/packages/@ngtools/logger/src/logger.spec.ts new file mode 100644 index 000000000000..c1cefe836bbf --- /dev/null +++ b/packages/@ngtools/logger/src/logger.spec.ts @@ -0,0 +1,46 @@ +import {Logger, JsonValue} from './logger'; +import 'rxjs/add/operator/toArray'; +import 'rxjs/add/operator/toPromise'; + + +describe('Logger', () => { + it('works', (done: DoneFn) => { + const logger = new Logger('test'); + logger + .toArray() + .toPromise() + .then((observed: JsonValue[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'hello', level: 'debug', name: 'test' }), + jasmine.objectContaining({ message: 'world', level: 'info', name: 'test' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + logger.debug('hello'); + logger.info('world'); + logger.complete(); + }); + + it('works with children', (done: DoneFn) => { + const logger = new Logger('test'); + let hasCompleted = false; + logger + .toArray() + .toPromise() + .then((observed: JsonValue[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'hello', level: 'debug', name: 'child' }), + jasmine.objectContaining({ message: 'world', level: 'info', name: 'child' }), + ]); + expect(hasCompleted).toBe(true); + }) + .then(() => done(), (err: any) => done.fail(err)); + + const childLogger = new Logger('child', logger); + childLogger.subscribe(null, null, () => hasCompleted = true); + childLogger.debug('hello'); + childLogger.info('world'); + logger.complete(); + }); +}); diff --git a/packages/@ngtools/logger/src/logger.ts b/packages/@ngtools/logger/src/logger.ts new file mode 100644 index 000000000000..a52418dac3de --- /dev/null +++ b/packages/@ngtools/logger/src/logger.ts @@ -0,0 +1,116 @@ +import {Observable} from 'rxjs/Observable'; +import {Operator} from 'rxjs/Operator'; +import {PartialObserver} from 'rxjs/Observer'; +import {Subject} from 'rxjs/Subject'; +import {Subscription} from 'rxjs/Subscription'; + + +export type JsonValue = boolean | number | string | JsonObject | JsonArray; +export interface JsonObject { + [key: string]: JsonValue; +} +export interface JsonArray extends Array {} + +export interface LoggerMetadata extends JsonObject { + name: string; + path: string[]; +} +export interface LogEntry extends LoggerMetadata { + level: LogLevel; + message: string; + timestamp: number; +} + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + + +export class Logger extends Observable { + protected readonly _subject: Subject = new Subject(); + protected _metadata: LoggerMetadata; + + private _obs: Observable; + private _subscription: Subscription; + + protected get _observable() { return this._obs; } + protected set _observable(v: Observable) { + if (this._subscription) { + this._subscription.unsubscribe(); + } + this._obs = v; + if (this.parent) { + this._subscription = this.subscribe((value: LogEntry) => { + this.parent._subject.next(value); + }, (error: any) => { + this.parent._subject.error(error); + }, () => { + this._subscription.unsubscribe(); + this._subscription = null; + }); + } + } + + constructor(public readonly name: string, public readonly parent: Logger | null = null) { + super(); + + let path: string[] = []; + let p = parent; + while (p) { + path.push(p.name); + p = p.parent; + } + this._metadata = { name, path }; + this._observable = this._subject.asObservable(); + if (parent) { + // When the parent completes, complete us as well. + this.parent._subject.subscribe(null, null, () => this.complete()); + } + } + + complete() { + this._subject.complete(); + } + + log(level: LogLevel, message: string, metadata: JsonObject = {}): void { + const entry: LogEntry = Object.assign({}, this._metadata, metadata, { + level, message, timestamp: +Date.now() + }); + this._subject.next(entry); + } + + debug(message: string, metadata: JsonObject = {}) { + return this.log('debug', message, metadata); + } + info(message: string, metadata: JsonObject = {}) { + return this.log('info', message, metadata); + } + warn(message: string, metadata: JsonObject = {}) { + return this.log('warn', message, metadata); + } + error(message: string, metadata: JsonObject = {}) { + return this.log('error', message, metadata); + } + fatal(message: string, metadata: JsonObject = {}) { + return this.log('fatal', message, metadata); + } + + toString() { + return ``; + } + + lift(operator: Operator): Observable { + return this._observable.lift(operator); + } + + subscribe(): Subscription; + subscribe(observer: PartialObserver): Subscription; + subscribe(next?: (value: LogEntry) => void, error?: (error: any) => void, + complete?: () => void): Subscription; + subscribe(observerOrNext?: PartialObserver | ((value: LogEntry) => void), + error?: (error: any) => void, + complete?: () => void): Subscription { + return this._observable.subscribe.apply(this._observable, arguments); + } + forEach(next: (value: LogEntry) => void, PromiseCtor?: typeof Promise): Promise { + return this._observable.forEach(next, PromiseCtor); + } +} diff --git a/packages/@ngtools/logger/src/null-logger.spec.ts b/packages/@ngtools/logger/src/null-logger.spec.ts new file mode 100644 index 000000000000..3b66f4a39dfe --- /dev/null +++ b/packages/@ngtools/logger/src/null-logger.spec.ts @@ -0,0 +1,37 @@ +import {NullLogger} from './null-logger'; +import {LogEntry, Logger} from './logger'; + + +describe('NullLogger', () => { + it('works', (done: DoneFn) => { + const logger = new NullLogger(); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + logger.debug('hello'); + logger.info('world'); + logger.complete(); + }); + + it('nullifies children', (done: DoneFn) => { + const logger = new Logger('test'); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + const nullLogger = new NullLogger(logger); + const child = new Logger('test', nullLogger); + child.debug('hello'); + child.info('world'); + logger.complete(); + }); +}); diff --git a/packages/@ngtools/logger/src/null-logger.ts b/packages/@ngtools/logger/src/null-logger.ts new file mode 100644 index 000000000000..e01b4229163e --- /dev/null +++ b/packages/@ngtools/logger/src/null-logger.ts @@ -0,0 +1,13 @@ +import {Logger} from './logger'; + +import {Observable} from 'rxjs/Observable'; + +import 'rxjs/add/observable/empty'; + + +export class NullLogger extends Logger { + constructor(parent: Logger | null = null) { + super('', parent); + this._observable = Observable.empty(); + } +} diff --git a/packages/@ngtools/logger/src/transform-logger.spec.ts b/packages/@ngtools/logger/src/transform-logger.spec.ts new file mode 100644 index 000000000000..f0cf572e7023 --- /dev/null +++ b/packages/@ngtools/logger/src/transform-logger.spec.ts @@ -0,0 +1,32 @@ +import {TransformLogger} from './transform-logger'; +import {LogEntry} from './logger'; + +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; + + +describe('TransformLogger', () => { + it('works', (done: DoneFn) => { + const logger = new TransformLogger('test', stream => { + return stream + .filter(entry => entry.message != 'hello') + .map(entry => { + entry.message += '1'; + return entry; + }); + }); + logger + .toArray() + .toPromise() + .then((observed: LogEntry[]) => { + expect(observed).toEqual([ + jasmine.objectContaining({ message: 'world1', level: 'info', name: 'test' }), + ]); + }) + .then(() => done(), (err: any) => done.fail(err)); + + logger.debug('hello'); + logger.info('world'); + logger.complete(); + }); +}); diff --git a/packages/@ngtools/logger/src/transform-logger.ts b/packages/@ngtools/logger/src/transform-logger.ts new file mode 100644 index 000000000000..e4d0a1fb7fcc --- /dev/null +++ b/packages/@ngtools/logger/src/transform-logger.ts @@ -0,0 +1,13 @@ +import {Observable} from 'rxjs/Observable'; + +import {Logger, LogEntry} from './logger'; + + +export class TransformLogger extends Logger { + constructor(name: string, + transform: (stream: Observable) => Observable, + parent: Logger | null = null) { + super(name, parent); + this._observable = transform(this._observable); + } +} diff --git a/packages/@ngtools/logger/tsconfig.json b/packages/@ngtools/logger/tsconfig.json new file mode 100644 index 000000000000..186872c43d53 --- /dev/null +++ b/packages/@ngtools/logger/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "experimentalDecorators": true, + "mapRoot": "", + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "outDir": "../../../dist/@ngtools/logger", + "rootDir": ".", + "lib": ["es2015", "es6", "dom"], + "target": "es5", + "sourceMap": true, + "sourceRoot": "/", + "baseUrl": ".", + "typeRoots": [ + "../../node_modules/@types" + ], + "types": [ + "jasmine", + "node" + ] + } +} diff --git a/scripts/publish/build.js b/scripts/publish/build.js index 898fb97a8cd9..86ad81990b79 100755 --- a/scripts/publish/build.js +++ b/scripts/publish/build.js @@ -32,6 +32,20 @@ function copy(from, to) { } +function rm(p) { + path.relative(process.cwd(), p); + return new Promise((resolve, reject) => { + fs.unlink(p, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + + function getDeps(pkg) { const packageJson = require(pkg.packageJson); return Object.assign({}, packageJson['dependencies'], packageJson['devDependencies']); @@ -76,7 +90,7 @@ Promise.resolve() .then(() => console.log('Copying uncompiled resources...')) .then(() => glob(path.join(packagesRoot, '**/*'), { dot: true })) .then(files => { - console.log(` Found ${files.length} files...`); + console.log(`Found ${files.length} files...`); return files .map((fileName) => path.relative(packagesRoot, fileName)) .filter((fileName) => { @@ -130,6 +144,14 @@ Promise.resolve() return promise.then(() => current); }, Promise.resolve()); }) + .then(() => glob(path.join(dist, '**/*.spec.*'))) + .then(specFiles => specFiles.filter(fileName => { + return !/[\\\/]angular-cli[\\\/]blueprints/.test(fileName); + })) + .then(specFiles => { + console.log(`Found ${specFiles.length} spec files...`); + return Promise.all(specFiles.map(rm)); + }) .then(() => { // Copy all resources that might have been missed. return Promise.all([ diff --git a/scripts/run-packages-spec.js b/scripts/run-packages-spec.js index 987b3ea3d87e..c361dea69240 100644 --- a/scripts/run-packages-spec.js +++ b/scripts/run-packages-spec.js @@ -6,14 +6,12 @@ const glob = require('glob'); const path = require('path'); const Jasmine = require('jasmine'); -const JasmineSpecReporter = require('jasmine-spec-reporter'); const projectBaseDir = path.join(__dirname, '../packages'); // Create a Jasmine runner and configure it. const jasmine = new Jasmine({ projectBaseDir: projectBaseDir }); jasmine.loadConfig({}); -jasmine.addReporter(new JasmineSpecReporter()); // Manually set exit code (needed with custom reporters) jasmine.onComplete((success) => process.exitCode = !success); diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts index ab643865dc4d..ff26447ff838 100644 --- a/tests/e2e/utils/process.ts +++ b/tests/e2e/utils/process.ts @@ -1,5 +1,5 @@ import * as child_process from 'child_process'; -import {blue, white, yellow} from 'chalk'; +import {blue, yellow} from 'chalk'; const treeKill = require('tree-kill'); @@ -15,9 +15,9 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise x !== undefined); const flags = [ @@ -28,8 +28,8 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise `"${x}"`).join(' ')}\`${flags}...`)); - console.log(blue(` CWD: ${cwd}`)); + console.log(blue(`Running \`${cmd} ${args.map(x => `"${x}"`).join(' ')}\`${flags}...`)); + console.log(blue(`CWD: ${cwd}`)); const spawnOptions: any = {cwd}; if (process.platform.startsWith('win')) { diff --git a/tests/e2e_runner.js b/tests/e2e_runner.ts similarity index 62% rename from tests/e2e_runner.js rename to tests/e2e_runner.ts index 5b869dd03517..4f97a80cc955 100644 --- a/tests/e2e_runner.js +++ b/tests/e2e_runner.ts @@ -1,24 +1,24 @@ -/*eslint-disable no-console */ -'use strict'; -require('../lib/bootstrap-local'); +// This may seem awkward but we're using Logger in our e2e. At this point the unit tests +// have run already so it should be "safe", teehee. +import { + ConsoleLoggerStack, + LogEntry, + IndentLogger, + NullLogger +} from '../packages/@ngtools/logger/src/index'; +import {blue, bold, green, red, yellow, white} from 'chalk'; +import {gitClean} from './e2e/utils/git'; +import * as glob from 'glob'; +import * as minimist from 'minimist'; +import * as path from 'path'; +import {setGlobalVariable} from './e2e/utils/env'; + +// RxJS +import 'rxjs/add/operator/filter'; +import 'rxjs/add/observable/empty'; -Error.stackTraceLimit = Infinity; - -/** - * This file is ran using the command line, not using Jasmine / Mocha. - */ -const chalk = require('chalk'); -const gitClean = require('./e2e/utils/git').gitClean; -const glob = require('glob'); -const minimist = require('minimist'); -const path = require('path'); -const blue = chalk.blue; -const bold = chalk.bold; -const green = chalk.green; -const red = chalk.red; -const white = chalk.white; -const setGlobalVariable = require('./e2e/utils/env').setGlobalVariable; +Error.stackTraceLimit = Infinity; /** @@ -35,7 +35,7 @@ const setGlobalVariable = require('./e2e/utils/env').setGlobalVariable; * If unnamed flags are passed in, the list of tests will be filtered to include only those passed. */ const argv = minimist(process.argv.slice(2), { - 'boolean': ['debug', 'nolink', 'nightly', 'noproject'], + 'boolean': ['debug', 'nolink', 'nightly', 'noproject', 'verbose'], 'string': ['reuse', 'ng-sha'] }); @@ -52,14 +52,30 @@ const argv = minimist(process.argv.slice(2), { process.exitCode = 255; +ConsoleLoggerStack.start(new IndentLogger('name')) + .filter((entry: LogEntry) => (entry.level != 'debug' || argv.verbose)) + .subscribe((entry: LogEntry) => { + let color: (s: string) => string = white; + let output = process.stdout; + switch (entry.level) { + case 'info': color = white; break; + case 'warn': color = yellow; break; + case 'error': color = red; output = process.stderr; break; + case 'fatal': color = (x: string) => bold(red(x)); output = process.stderr; break; + } + + output.write(color(entry.message) + '\n'); + }); + + let currentFileName = null; let index = 0; const e2eRoot = path.join(__dirname, 'e2e'); -const allSetups = glob.sync(path.join(e2eRoot, 'setup/**/*'), { nodir: true }) +const allSetups = glob.sync(path.join(e2eRoot, 'setup/**/*.ts'), { nodir: true }) .map(name => path.relative(e2eRoot, name)) .sort(); -const allTests = glob.sync(path.join(e2eRoot, 'tests/**/*'), { nodir: true }) +const allTests = glob.sync(path.join(e2eRoot, 'tests/**/*.ts'), { nodir: true }) .map(name => path.relative(e2eRoot, name)) .sort(); @@ -73,8 +89,8 @@ const testsToRun = allSetups return argv._.some(argName => { return path.join(process.cwd(), argName) == path.join(__dirname, 'e2e', name) - || argName == name - || argName == name.replace(/\.ts$/, ''); + || argName == name + || argName == name.replace(/\.ts$/, ''); }); })); @@ -103,19 +119,21 @@ testsToRun.reduce((previous, relativeName) => { const start = +new Date(); const module = require(absoluteName); - const fn = (typeof module == 'function') ? module + const fn: (...args: any[]) => Promise | any = + (typeof module == 'function') ? module : (typeof module.default == 'function') ? module.default - : function() { - throw new Error('Invalid test module.'); - }; + : () => { throw new Error('Invalid test module.'); }; let clean = true; let previousDir = null; return Promise.resolve() .then(() => printHeader(currentFileName)) .then(() => previousDir = process.cwd()) + .then(() => ConsoleLoggerStack.push(currentFileName)) .then(() => fn(() => clean = false)) - .then(() => console.log(' ----')) + .then(() => ConsoleLoggerStack.pop(), (err: any) => { ConsoleLoggerStack.pop(); throw err; }) + .then(() => console.log('----')) + .then(() => { ConsoleLoggerStack.push(NullLogger); }) .then(() => { // If we're not in a setup, change the directory back to where it was before the test. // This allows tests to chdir without worrying about keeping the original directory. @@ -130,36 +148,37 @@ testsToRun.reduce((previous, relativeName) => { return gitClean(); } }) + .then(() => ConsoleLoggerStack.pop(), (err: any) => { ConsoleLoggerStack.pop(); throw err; }) .then(() => printFooter(currentFileName, start), - (err) => { - printFooter(currentFileName, start); - console.error(err); - throw err; - }); + (err) => { + printFooter(currentFileName, start); + console.error(err); + throw err; + }); }); }, Promise.resolve()) -.then(() => { - console.log(green('Done.')); - process.exit(0); -}, -(err) => { - console.log('\n'); - console.error(red(`Test "${currentFileName}" failed...`)); - console.error(red(err.message)); - console.error(red(err.stack)); - - if (argv.debug) { - console.log(`Current Directory: ${process.cwd()}`); - console.log('Will loop forever while you debug... CTRL-C to quit.'); - - /* eslint-disable no-constant-condition */ - while (1) { - // That's right! - } - } + .then(() => { + console.log(green('Done.')); + process.exit(0); + }, + (err) => { + console.log('\n'); + console.error(red(`Test "${currentFileName}" failed...`)); + console.error(red(err.message)); + console.error(red(err.stack)); + + if (argv.debug) { + console.log(`Current Directory: ${process.cwd()}`); + console.log('Will loop forever while you debug... CTRL-C to quit.'); + + /* eslint-disable no-constant-condition */ + while (1) { + // That's right! + } + } - process.exit(1); -}); + process.exit(1); + }); function encode(str) { diff --git a/tests/run_e2e.js b/tests/run_e2e.js new file mode 100644 index 000000000000..065b7d84dfdf --- /dev/null +++ b/tests/run_e2e.js @@ -0,0 +1,4 @@ +/*eslint-disable no-console */ +'use strict'; +require('../lib/bootstrap-local'); +require('./e2e_runner.ts');