From f442afe315a1e3e0989cdc5c5c29e29054f702ec Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Thu, 7 Mar 2019 20:07:31 -0800 Subject: [PATCH 1/4] feat(serve): support --consolelogs option Sets up a websocket server and injects a script in to the app to pass console logs back to the server to be printed in the terminal. --consolelogs-port configures the websocket server port --- .gitignore | 3 + assets/consolelog-config.js | 0 assets/consolelogs.js | 71 ++++++++++++++++ {builders/cordova-build => assets}/cordova.js | 0 assets/log-server.ts | 80 +++++++++++++++++++ builders/cordova-build/index.ts | 22 ++++- builders/cordova-build/schema.d.ts | 2 + builders/cordova-build/schema.json | 9 +++ builders/cordova-serve/index.ts | 13 ++- builders/cordova-serve/schema.d.ts | 2 + builders/cordova-serve/schema.json | 9 +++ package.json | 5 +- 12 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 assets/consolelog-config.js create mode 100644 assets/consolelogs.js rename {builders/cordova-build => assets}/cordova.js (100%) create mode 100644 assets/log-server.ts diff --git a/.gitignore b/.gitignore index 77c6633..77b61b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ package-lock.json node_modules **/*.js +!assets/**/*.js +assets/log-server.js **/*.d.ts !builders/**/schema.d.ts !schematics/**/schema.d.ts !schematics/*/files/**/* +.vscode diff --git a/assets/consolelog-config.js b/assets/consolelog-config.js new file mode 100644 index 0000000..e69de29 diff --git a/assets/consolelogs.js b/assets/consolelogs.js new file mode 100644 index 0000000..751a9ae --- /dev/null +++ b/assets/consolelogs.js @@ -0,0 +1,71 @@ +window.Ionic = window.Ionic || {}; window.Ionic.ConsoleLogServer = { + start: function(config) { + var self = this; + + this.socket = new WebSocket('ws://' + window.location.hostname + ':' + String(config.wsPort)); + this.msgQueue = []; + + this.socket.onopen = function() { + self.socketReady = true; + + self.socket.onclose = function() { + self.socketReady = false; + console.warn('Console log server closed'); + }; + }; + + this.patchConsole(); + }, + + queueMessageSend: function(msg) { + this.msgQueue.push(msg); + this.drainMessageQueue(); + }, + + drainMessageQueue: function() { + var msg; + while (msg = this.msgQueue.shift()) { + if (this.socketReady) { + try { + this.socket.send(JSON.stringify(msg)); + } catch(e) { + if (!(e instanceof TypeError)) { + console.error('ws error: ' + e); + } + } + } + } + }, + + patchConsole: function() { + var self = this; + + function _patchConsole(consoleType) { + console[consoleType] = (function() { + var orgConsole = console[consoleType]; + return function() { + orgConsole.apply(console, arguments); + var msg = { + category: 'console', + type: consoleType, + data: [] + }; + for (var i = 0; i < arguments.length; i++) { + msg.data.push(arguments[i]); + } + if (msg.data.length) { + self.queueMessageSend(msg); + } + }; + })(); + } + + // https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-console/#supported-methods + var consoleFns = ['log', 'error', 'exception', 'warn', 'info', 'debug', 'assert', 'dir', 'dirxml', 'time', 'timeEnd', 'table']; + for (var i in consoleFns) { + _patchConsole(consoleFns[i]); + } + }, +}; + +Ionic.ConsoleLogServer.start(Ionic.ConsoleLogServerConfig || {}); diff --git a/builders/cordova-build/cordova.js b/assets/cordova.js similarity index 100% rename from builders/cordova-build/cordova.js rename to assets/cordova.js diff --git a/assets/log-server.ts b/assets/log-server.ts new file mode 100644 index 0000000..d32a805 --- /dev/null +++ b/assets/log-server.ts @@ -0,0 +1,80 @@ +import chalk, { Chalk } from 'chalk'; +import * as util from 'util'; +import * as ζws from 'ws'; + +export interface ConsoleLogServerMessage { + category: 'console'; + type: string; + data: any[]; +} + +export interface ConsoleLogServerOptions { + consolelogs: boolean; + consolelogsPort: number; +} + +export function isConsoleLogServerMessage(m: any): m is ConsoleLogServerMessage { + return m + && typeof m.category === 'string' + && typeof m.type === 'string' + && m.data && typeof m.data.length === 'number'; +} + +export async function createConsoleLogServer(host: string, port: number): Promise<ζws.Server> { + const WebSocket = await import('ws'); + + const wss = new WebSocket.Server({ host, port }); + + wss.on('connection', ws => { + ws.on('message', data => { + let msg; + + try { + data = data.toString(); + msg = JSON.parse(data); + } catch (e) { + process.stderr.write(`Error parsing JSON message from client: "${data}" ${chalk.red(e.stack ? e.stack : e)}\n`); + return; + } + + if (!isConsoleLogServerMessage(msg)) { + const m = util.inspect(msg, { colors: chalk.enabled }); + process.stderr.write(`Bad format in client message: ${m}\n`); + return; + } + + if (msg.category === 'console') { + let status: Chalk | undefined; // unknown levels are normal color + + if (msg.type === 'info' || msg.type === 'log') { + status = chalk.reset; + } else if (msg.type === 'error') { + status = chalk.red; + } else if (msg.type === 'warn') { + status = chalk.yellow; + } + + // pretty print objects and arrays (no newlines for arrays) + msg.data = msg.data.map(d => JSON.stringify(d, undefined, d && d.length ? '' : ' ')); + + if (status) { + process.stdout.write(`[${status('console.' + msg.type)}]: ${msg.data.join(' ')}\n`); + } else { + process.stdout.write(`[console]: ${msg.data.join(' ')}\n`); + } + } + }); + + ws.on('error', (err: NodeJS.ErrnoException) => { + if (err && err.code !== 'ECONNRESET') { + process.stderr.write(`There was an error with the logging stream: ${JSON.stringify(err)}\n`); + } + }); + }); + + wss.on('error', (err: NodeJS.ErrnoException) => { + process.stderr.write(`There was an error with the logging websocket: ${JSON.stringify(err)}\n`); + }); + + return wss; +} diff --git a/builders/cordova-build/index.ts b/builders/cordova-build/index.ts index 219e4b0..a07bcf6 100644 --- a/builders/cordova-build/index.ts +++ b/builders/cordova-build/index.ts @@ -1,6 +1,7 @@ import { BuildEvent, Builder, BuilderConfiguration, BuilderContext, BuilderDescription } from '@angular-devkit/architect'; import { BrowserBuilderSchema } from '@angular-devkit/build-angular/src/browser/schema'; import { getSystemPath, join, normalize } from '@angular-devkit/core'; +import { createWriteStream } from 'fs'; import { Observable, of } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; @@ -54,9 +55,28 @@ export class CordovaBuildBuilder implements Builder { // by default. Let's keep it around. browserOptions.deleteOutputPath = false; + if (options.consolelogs) { + // Write the config to a file, and then include that in the bundle so it loads on window + const configPath = getSystemPath(join(normalize(__dirname), '../../assets', normalize('consolelog-config.js'))); + const ws = createWriteStream(configPath, { encoding: 'utf8' }); + ws.end(`window.Ionic = window.Ionic || {}; Ionic.ConsoleLogServerConfig = { wsPort: ${options.consolelogsPort} }`); + + browserOptions.scripts.push({ + input: configPath, + bundleName: 'consolelogs', + lazy: false, + }); + + browserOptions.scripts.push({ + input: getSystemPath(join(normalize(__dirname), '../../assets', normalize('consolelogs.js'))), + bundleName: 'consolelogs', + lazy: false, + }); + } + if (options.cordovaMock) { browserOptions.scripts.push({ - input: getSystemPath(join(normalize(__dirname), normalize('cordova.js'))), + input: getSystemPath(join(normalize(__dirname), '../../assets', normalize('cordova.js'))), bundleName: 'cordova', lazy: false, }); diff --git a/builders/cordova-build/schema.d.ts b/builders/cordova-build/schema.d.ts index 8f17bf3..a00fa46 100644 --- a/builders/cordova-build/schema.d.ts +++ b/builders/cordova-build/schema.d.ts @@ -5,4 +5,6 @@ export interface CordovaBuildBuilderSchema { sourceMap?: boolean; cordovaAssets?: boolean; cordovaMock?: boolean; + consolelogs?: boolean; + consolelogsPort?: number; } diff --git a/builders/cordova-build/schema.json b/builders/cordova-build/schema.json index 7e3583b..f1ec1ee 100644 --- a/builders/cordova-build/schema.json +++ b/builders/cordova-build/schema.json @@ -7,6 +7,15 @@ "type": "string", "description": "Target to build." }, + "consolelogs": { + "type": "boolean", + "description": "Inject script to print app console logs to Ionic CLI" + }, + "consolelogsPort": { + "type": "number", + "description": "Port for console log server", + "default": 53703 + }, "platform": { "type": "string", "description": "Cordova platform to use during build." diff --git a/builders/cordova-serve/index.ts b/builders/cordova-serve/index.ts index 08df658..32f6a1a 100644 --- a/builders/cordova-serve/index.ts +++ b/builders/cordova-serve/index.ts @@ -6,6 +6,7 @@ import * as fs from 'fs'; import { Observable, of } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; +import { createConsoleLogServer } from '../../assets/log-server'; import { CordovaBuildBuilder, CordovaBuildBuilderSchema } from '../cordova-build'; import { CordovaServeBuilderSchema } from './schema'; @@ -36,9 +37,9 @@ export class CordovaServeBuilder implements Builder { } protected _getCordovaBuildConfig(cordovaServeOptions: CordovaServeBuilderSchema): Observable> { - const { platform, cordovaBasePath, cordovaAssets, cordovaMock } = cordovaServeOptions; + const { platform, cordovaBasePath, cordovaAssets, cordovaMock, consolelogs, consolelogsPort } = cordovaServeOptions; const [ project, target, configuration ] = cordovaServeOptions.cordovaBuildTarget.split(':'); - const cordovaBuildTargetSpec = { project, target, configuration, overrides: { platform, cordovaBasePath, cordovaAssets, cordovaMock } }; + const cordovaBuildTargetSpec = { project, target, configuration, overrides: { platform, cordovaBasePath, cordovaAssets, cordovaMock, consolelogs, consolelogsPort } }; const cordovaBuildTargetConfig = this.context.architect.getBuilderConfiguration(cordovaBuildTargetSpec); return this.context.architect.getBuilderDescription(cordovaBuildTargetConfig).pipe( @@ -52,6 +53,14 @@ class CordovaDevServerBuilder extends DevServerBuilder { super(context); } + run(builderConfig: BuilderConfiguration): Observable { + if (this.cordovaBuildOptions.consolelogs && this.cordovaBuildOptions.consolelogsPort) { + createConsoleLogServer(builderConfig.options.host, this.cordovaBuildOptions.consolelogsPort) + .catch(err => process.stderr.write(`There was an error starting the console log server: ${err}\n`)); + } + return super.run(builderConfig); + } + buildWebpackConfig(root: Path, projectRoot: Path, host: virtualFs.Host, browserOptions: NormalizedBrowserBuilderSchema) { const builder = new CordovaBuildBuilder(this.context); builder.validateBuilderConfig(this.cordovaBuildOptions); diff --git a/builders/cordova-serve/schema.d.ts b/builders/cordova-serve/schema.d.ts index c57680e..fb5db01 100644 --- a/builders/cordova-serve/schema.d.ts +++ b/builders/cordova-serve/schema.d.ts @@ -9,4 +9,6 @@ export interface CordovaServeBuilderSchema { sourceMap?: boolean; cordovaAssets?: boolean; cordovaMock?: boolean; + consolelogs?: boolean; + consolelogsPort?: number; } diff --git a/builders/cordova-serve/schema.json b/builders/cordova-serve/schema.json index 00c08fc..2dc60e1 100644 --- a/builders/cordova-serve/schema.json +++ b/builders/cordova-serve/schema.json @@ -7,6 +7,15 @@ "type": "string", "description": "Target to use for build." }, + "consolelogs": { + "type": "boolean", + "description": "Print app console logs to Ionic CLI" + }, + "consolelogsPort": { + "type": "number", + "description": "Port for console log server", + "default": 53703 + }, "devServerTarget": { "type": "string", "description": "Target to use for serve." diff --git a/package.json b/package.json index f6ac849..4d70b37 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ ], "dependencies": { "@schematics/angular": "^7.0.3", + "chalk": "^2.4.2", "tslib": "^1.9.0", - "typescript": "^3.2.4" + "typescript": "^3.2.4", + "ws": "^6.1.4" }, "devDependencies": { "@angular-devkit/architect": "0.13.1", @@ -46,6 +48,7 @@ "@types/node": "^8.10.34", "@types/webpack": "^4.4.14", "@types/webpack-dev-server": "^3.1.1", + "@types/ws": "^6.0.1", "commitizen": "^3.0.2", "cz-conventional-changelog": "^2.1.0", "husky": "^1.1.1", From 8bd6155247e151d975e54b76135dbce1f1ca8279 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Fri, 8 Mar 2019 18:00:54 -0800 Subject: [PATCH 2/4] chore(serve): PR changes --- .gitignore | 2 -- assets/consolelogs.js | 2 ++ builders/cordova-build/schema.json | 4 ++-- builders/cordova-serve/index.ts | 8 ++++---- .../cordova-serve}/log-server.ts | 20 +++++++++---------- builders/cordova-serve/schema.json | 4 ++-- 6 files changed, 19 insertions(+), 21 deletions(-) rename {assets => builders/cordova-serve}/log-server.ts (81%) diff --git a/.gitignore b/.gitignore index 77b61b2..ee9c07d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ package-lock.json node_modules **/*.js -!assets/**/*.js -assets/log-server.js **/*.d.ts !builders/**/schema.d.ts !schematics/**/schema.d.ts diff --git a/assets/consolelogs.js b/assets/consolelogs.js index 751a9ae..c01b9f4 100644 --- a/assets/consolelogs.js +++ b/assets/consolelogs.js @@ -1,3 +1,5 @@ +// Script injected by @ionic/angular-toolkit to send console logs back +// to a websocket server so they can be printed to the terminal window.Ionic = window.Ionic || {}; window.Ionic.ConsoleLogServer = { start: function(config) { var self = this; diff --git a/builders/cordova-build/schema.json b/builders/cordova-build/schema.json index f1ec1ee..5cb6dac 100644 --- a/builders/cordova-build/schema.json +++ b/builders/cordova-build/schema.json @@ -9,11 +9,11 @@ }, "consolelogs": { "type": "boolean", - "description": "Inject script to print app console logs to Ionic CLI" + "description": "Inject script to print console logs to the terminal." }, "consolelogsPort": { "type": "number", - "description": "Port for console log server", + "description": "Port for console log server.", "default": 53703 }, "platform": { diff --git a/builders/cordova-serve/index.ts b/builders/cordova-serve/index.ts index 32f6a1a..c7810a3 100644 --- a/builders/cordova-serve/index.ts +++ b/builders/cordova-serve/index.ts @@ -3,12 +3,12 @@ import { NormalizedBrowserBuilderSchema } from '@angular-devkit/build-angular/sr import { DevServerBuilder, DevServerBuilderOptions } from '@angular-devkit/build-angular/src/dev-server'; import { Path, virtualFs } from '@angular-devkit/core'; import * as fs from 'fs'; -import { Observable, of } from 'rxjs'; +import { Observable, from, of } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; -import { createConsoleLogServer } from '../../assets/log-server'; import { CordovaBuildBuilder, CordovaBuildBuilderSchema } from '../cordova-build'; +import { createConsoleLogServer } from './log-server'; import { CordovaServeBuilderSchema } from './schema'; export class CordovaServeBuilder implements Builder { @@ -55,8 +55,8 @@ class CordovaDevServerBuilder extends DevServerBuilder { run(builderConfig: BuilderConfiguration): Observable { if (this.cordovaBuildOptions.consolelogs && this.cordovaBuildOptions.consolelogsPort) { - createConsoleLogServer(builderConfig.options.host, this.cordovaBuildOptions.consolelogsPort) - .catch(err => process.stderr.write(`There was an error starting the console log server: ${err}\n`)); + return from(createConsoleLogServer(builderConfig.options.host, this.cordovaBuildOptions.consolelogsPort)) + .pipe(_ => super.run(builderConfig)); } return super.run(builderConfig); } diff --git a/assets/log-server.ts b/builders/cordova-serve/log-server.ts similarity index 81% rename from assets/log-server.ts rename to builders/cordova-serve/log-server.ts index d32a805..e7d9c63 100644 --- a/assets/log-server.ts +++ b/builders/cordova-serve/log-server.ts @@ -1,6 +1,6 @@ -import chalk, { Chalk } from 'chalk'; +import { terminal } from '@angular-devkit/core'; import * as util from 'util'; -import * as ζws from 'ws'; +import * as WebSocket from 'ws'; export interface ConsoleLogServerMessage { category: 'console'; @@ -20,9 +20,7 @@ export function isConsoleLogServerMessage(m: any): m is ConsoleLogServerMessage && m.data && typeof m.data.length === 'number'; } -export async function createConsoleLogServer(host: string, port: number): Promise<ζws.Server> { - const WebSocket = await import('ws'); - +export async function createConsoleLogServer(host: string, port: number): Promise { const wss = new WebSocket.Server({ host, port }); wss.on('connection', ws => { @@ -33,25 +31,25 @@ export async function createConsoleLogServer(host: string, port: number): Promis data = data.toString(); msg = JSON.parse(data); } catch (e) { - process.stderr.write(`Error parsing JSON message from client: "${data}" ${chalk.red(e.stack ? e.stack : e)}\n`); + process.stderr.write(`Error parsing JSON message from client: "${data}" ${terminal.red(e.stack ? e.stack : e)}\n`); return; } if (!isConsoleLogServerMessage(msg)) { - const m = util.inspect(msg, { colors: chalk.enabled }); + const m = util.inspect(msg, { colors: true }); process.stderr.write(`Bad format in client message: ${m}\n`); return; } if (msg.category === 'console') { - let status: Chalk | undefined; // unknown levels are normal color + let status: ((_: string) => string) | undefined; if (msg.type === 'info' || msg.type === 'log') { - status = chalk.reset; + status = terminal.reset; } else if (msg.type === 'error') { - status = chalk.red; + status = terminal.red; } else if (msg.type === 'warn') { - status = chalk.yellow; + status = terminal.yellow; } // pretty print objects and arrays (no newlines for arrays) diff --git a/builders/cordova-serve/schema.json b/builders/cordova-serve/schema.json index 2dc60e1..edd85e6 100644 --- a/builders/cordova-serve/schema.json +++ b/builders/cordova-serve/schema.json @@ -9,11 +9,11 @@ }, "consolelogs": { "type": "boolean", - "description": "Print app console logs to Ionic CLI" + "description": "Print console logs to the terminal." }, "consolelogsPort": { "type": "number", - "description": "Port for console log server", + "description": "Port for console log server.", "default": 53703 }, "devServerTarget": { From c600bb959dc965716e32066b9c86ec791f4f52e0 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Sat, 9 Mar 2019 09:01:33 -0800 Subject: [PATCH 3/4] fix(serve): remove chalk dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 4d70b37..570da09 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ ], "dependencies": { "@schematics/angular": "^7.0.3", - "chalk": "^2.4.2", "tslib": "^1.9.0", "typescript": "^3.2.4", "ws": "^6.1.4" From e2343b2dffca4a0b8b1ecd85871bbc864c145031 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Thu, 21 Mar 2019 09:38:59 -0700 Subject: [PATCH 4/4] remove unnecessary stream file write --- builders/cordova-build/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/builders/cordova-build/index.ts b/builders/cordova-build/index.ts index a07bcf6..2d5949c 100644 --- a/builders/cordova-build/index.ts +++ b/builders/cordova-build/index.ts @@ -1,7 +1,7 @@ import { BuildEvent, Builder, BuilderConfiguration, BuilderContext, BuilderDescription } from '@angular-devkit/architect'; import { BrowserBuilderSchema } from '@angular-devkit/build-angular/src/browser/schema'; import { getSystemPath, join, normalize } from '@angular-devkit/core'; -import { createWriteStream } from 'fs'; +import { writeFileSync } from 'fs'; import { Observable, of } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; @@ -58,8 +58,7 @@ export class CordovaBuildBuilder implements Builder { if (options.consolelogs) { // Write the config to a file, and then include that in the bundle so it loads on window const configPath = getSystemPath(join(normalize(__dirname), '../../assets', normalize('consolelog-config.js'))); - const ws = createWriteStream(configPath, { encoding: 'utf8' }); - ws.end(`window.Ionic = window.Ionic || {}; Ionic.ConsoleLogServerConfig = { wsPort: ${options.consolelogsPort} }`); + writeFileSync(configPath, `window.Ionic = window.Ionic || {}; Ionic.ConsoleLogServerConfig = { wsPort: ${options.consolelogsPort} }`); browserOptions.scripts.push({ input: configPath,