diff --git a/.gitignore b/.gitignore index 77c6633..ee9c07d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules !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..c01b9f4 --- /dev/null +++ b/assets/consolelogs.js @@ -0,0 +1,73 @@ +// 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; + + 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/builders/cordova-build/index.ts b/builders/cordova-build/index.ts index 219e4b0..2d5949c 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 { writeFileSync } from 'fs'; import { Observable, of } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; @@ -54,9 +55,27 @@ 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'))); + writeFileSync(configPath, `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..5cb6dac 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 console logs to the terminal." + }, + "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..c7810a3 100644 --- a/builders/cordova-serve/index.ts +++ b/builders/cordova-serve/index.ts @@ -3,11 +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 { CordovaBuildBuilder, CordovaBuildBuilderSchema } from '../cordova-build'; +import { createConsoleLogServer } from './log-server'; import { CordovaServeBuilderSchema } from './schema'; export class CordovaServeBuilder implements Builder { @@ -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) { + return from(createConsoleLogServer(builderConfig.options.host, this.cordovaBuildOptions.consolelogsPort)) + .pipe(_ => super.run(builderConfig)); + } + 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/log-server.ts b/builders/cordova-serve/log-server.ts new file mode 100644 index 0000000..e7d9c63 --- /dev/null +++ b/builders/cordova-serve/log-server.ts @@ -0,0 +1,78 @@ +import { terminal } from '@angular-devkit/core'; +import * as util from 'util'; +import * as WebSocket 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 { + 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}" ${terminal.red(e.stack ? e.stack : e)}\n`); + return; + } + + if (!isConsoleLogServerMessage(msg)) { + const m = util.inspect(msg, { colors: true }); + process.stderr.write(`Bad format in client message: ${m}\n`); + return; + } + + if (msg.category === 'console') { + let status: ((_: string) => string) | undefined; + + if (msg.type === 'info' || msg.type === 'log') { + status = terminal.reset; + } else if (msg.type === 'error') { + status = terminal.red; + } else if (msg.type === 'warn') { + status = terminal.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-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..edd85e6 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 console logs to the terminal." + }, + "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 3dd5f8f..e07493a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "dependencies": { "@schematics/angular": "^7.0.3", "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 +47,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",