From a4b43a59e497580bdece11c81f84445e83bd395f Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Tue, 21 Feb 2017 09:01:40 +0000 Subject: [PATCH] fix(serve): allow relevant live-reload options to function (#4744) This provides implementations for the following serve command options: live-reload [boolean; default: true] -- flag to control the browser live reload capability live-reload-client [URL; default: ssl/host/port command options] -- specify the URL that the live reload browser client will use Closes #3361 --- docs/documentation/serve.md | 8 +- packages/@angular/cli/commands/serve.ts | 133 ++++++++---------------- packages/@angular/cli/tasks/serve.ts | 89 +++++++++------- tests/e2e/tests/misc/live-reload.ts | 118 +++++++++++++++++++++ 4 files changed, 215 insertions(+), 133 deletions(-) create mode 100644 tests/e2e/tests/misc/live-reload.ts diff --git a/docs/documentation/serve.md b/docs/documentation/serve.md index e7e46eee6490..b0a3665798cf 100644 --- a/docs/documentation/serve.md +++ b/docs/documentation/serve.md @@ -16,13 +16,7 @@ `--live-reload` (`-lr`) flag to turn off live reloading -`--live-reload-host` (`-lrh`) specify the host for live reloading - -`--live-reload-base-url` (`-lrbu`) specify the base URL for live reloading - -`--live-reload-port` (`-lrp`) port for live reloading - -`--live-reload-live-css` flag to live reload CSS +`--live-reload-client` specify the URL that the live reload browser client will use `--ssl` flag to turn on SSL diff --git a/packages/@angular/cli/commands/serve.ts b/packages/@angular/cli/commands/serve.ts index 53bc97583541..20d12f119d75 100644 --- a/packages/@angular/cli/commands/serve.ts +++ b/packages/@angular/cli/commands/serve.ts @@ -10,23 +10,19 @@ import { overrideOptions } from '../utilities/override-options'; const SilentError = require('silent-error'); const PortFinder = require('portfinder'); const Command = require('../ember-cli/lib/models/command'); -const getPort = denodeify(PortFinder.getPort); - -PortFinder.basePort = 49152; +const getPort = denodeify<{ host: string, port: number }, number>(PortFinder.getPort); const config = CliConfig.fromProject() || CliConfig.fromGlobal(); const defaultPort = process.env.PORT || config.get('defaults.serve.port'); const defaultHost = config.get('defaults.serve.host'); +PortFinder.basePort = defaultPort; export interface ServeTaskOptions extends BuildOptions { port?: number; host?: string; proxyConfig?: string; liveReload?: boolean; - liveReloadHost?: string; - liveReloadPort?: number; - liveReloadBaseUrl?: string; - liveReloadLiveCss?: boolean; + liveReloadClient?: string; ssl?: boolean; sslKey?: string; sslCert?: string; @@ -35,80 +31,57 @@ export interface ServeTaskOptions extends BuildOptions { } // Expose options unrelated to live-reload to other commands that need to run serve -export const baseServeCommandOptions: any = baseBuildCommandOptions.concat([ - { name: 'port', type: Number, default: defaultPort, aliases: ['p'] }, - { - name: 'host', - type: String, - default: defaultHost, - aliases: ['H'], - description: `Listens only on ${defaultHost} by default` - }, - { name: 'proxy-config', type: 'Path', aliases: ['pc'] }, - { name: 'ssl', type: Boolean, default: false }, - { name: 'ssl-key', type: String, default: 'ssl/server.key' }, - { name: 'ssl-cert', type: String, default: 'ssl/server.crt' }, - { - name: 'open', - type: Boolean, - default: false, - aliases: ['o'], - description: 'Opens the url in default browser', - } -]); +export const baseServeCommandOptions: any = overrideOptions( + baseBuildCommandOptions.concat([ + { name: 'port', type: Number, default: defaultPort, aliases: ['p'] }, + { + name: 'host', + type: String, + default: defaultHost, + aliases: ['H'], + description: `Listens only on ${defaultHost} by default` + }, + { name: 'proxy-config', type: 'Path', aliases: ['pc'] }, + { name: 'ssl', type: Boolean, default: false }, + { name: 'ssl-key', type: String, default: 'ssl/server.key' }, + { name: 'ssl-cert', type: String, default: 'ssl/server.crt' }, + { + name: 'open', + type: Boolean, + default: false, + aliases: ['o'], + description: 'Opens the url in default browser', + }, + { name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] }, + { + name: 'live-reload-client', + type: String, + description: 'specify the URL that the live reload browser client will use' + }, + { + name: 'hmr', + type: Boolean, + default: false, + description: 'Enable hot module replacement', + } + ]), [ + { name: 'watch', default: true }, + ] +); const ServeCommand = Command.extend({ name: 'serve', description: 'Builds and serves your app, rebuilding on file changes.', aliases: ['server', 's'], - availableOptions: overrideOptions( - baseServeCommandOptions.concat([ - { name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] }, - { - name: 'live-reload-host', - type: String, - aliases: ['lrh'], - description: 'Defaults to host' - }, - { - name: 'live-reload-base-url', - type: String, - aliases: ['lrbu'], - description: 'Defaults to baseURL' - }, - { - name: 'live-reload-port', - type: Number, - aliases: ['lrp'], - description: '(Defaults to port number within [49152...65535])' - }, - { - name: 'live-reload-live-css', - type: Boolean, - default: true, - description: 'Whether to live reload CSS (default true)' - }, - { - name: 'hmr', - type: Boolean, - default: false, - description: 'Enable hot module replacement', - } - ]), [ - { name: 'watch', default: true }, - ] - ), + availableOptions: baseServeCommandOptions, run: function (commandOptions: ServeTaskOptions) { const ServeTask = require('../tasks/serve').default; Version.assertAngularVersionIs2_3_1OrHigher(this.project.root); - commandOptions.liveReloadHost = commandOptions.liveReloadHost || commandOptions.host; - return checkPort(commandOptions.port, commandOptions.host) - .then((port: number) => commandOptions.port = port) - .then(() => autoFindLiveReloadPort(commandOptions)) + return checkExpressPort(commandOptions) .then((opts: ServeTaskOptions) => { const serve = new ServeTask({ ui: this.ui, @@ -137,26 +110,4 @@ function checkExpressPort(commandOptions: ServeTaskOptions) { }); } -function autoFindLiveReloadPort(commandOptions: ServeTaskOptions) { - return getPort({ port: commandOptions.liveReloadPort, host: commandOptions.liveReloadHost }) - .then((foundPort: number) => { - - // if live reload port matches express port, try one higher - if (foundPort === commandOptions.port) { - commandOptions.liveReloadPort = foundPort + 1; - return autoFindLiveReloadPort(commandOptions); - } - - // port was already open - if (foundPort === commandOptions.liveReloadPort) { - return commandOptions; - } - - // use found port as live reload port - commandOptions.liveReloadPort = foundPort; - return commandOptions; - - }); -} - export default ServeCommand; diff --git a/packages/@angular/cli/tasks/serve.ts b/packages/@angular/cli/tasks/serve.ts index 8838266eef9b..c78d7728eca6 100644 --- a/packages/@angular/cli/tasks/serve.ts +++ b/packages/@angular/cli/tasks/serve.ts @@ -41,33 +41,52 @@ export default Task.extend({ let webpackConfig = new NgCliWebpackConfig(serveTaskOptions).config; - // This allows for live reload of page when changes are made to repo. - // https://webpack.github.io/docs/webpack-dev-server.html#inline-mode - let entryPoints = [ - `webpack-dev-server/client?http://${serveTaskOptions.host}:${serveTaskOptions.port}/` - ]; - if (serveTaskOptions.hmr) { - const webpackHmrLink = 'https://webpack.github.io/docs/hot-module-replacement.html'; - ui.writeLine(oneLine` - ${chalk.yellow('NOTICE')} Hot Module Replacement (HMR) is enabled for the dev server. - `); - ui.writeLine(' The project will still live reload when HMR is enabled,'); - ui.writeLine(' but to take advantage of HMR additional application code is required'); - ui.writeLine(' (not included in an Angular CLI project by default).'); - ui.writeLine(` See ${chalk.blue(webpackHmrLink)}`); - ui.writeLine(' for information on working with HMR for Webpack.'); - entryPoints.push('webpack/hot/dev-server'); - webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); - webpackConfig.plugins.push(new webpack.NamedModulesPlugin()); - if (serveTaskOptions.extractCss) { + const serverAddress = url.format({ + protocol: serveTaskOptions.ssl ? 'https' : 'http', + hostname: serveTaskOptions.host, + port: serveTaskOptions.port.toString() + }); + let clientAddress = serverAddress; + if (serveTaskOptions.liveReloadClient) { + const clientUrl = url.parse(serveTaskOptions.liveReloadClient); + // very basic sanity check + if (!clientUrl.host) { + return Promise.reject(new SilentError(`'live-reload-client' must be a full URL.`)); + } + clientAddress = clientUrl.href; + } + + if (serveTaskOptions.liveReload) { + // This allows for live reload of page when changes are made to repo. + // https://webpack.github.io/docs/webpack-dev-server.html#inline-mode + let entryPoints = [ + `webpack-dev-server/client?${clientAddress}` + ]; + if (serveTaskOptions.hmr) { + const webpackHmrLink = 'https://webpack.github.io/docs/hot-module-replacement.html'; ui.writeLine(oneLine` - ${chalk.yellow('NOTICE')} (HMR) does not allow for CSS hot reload when used - together with '--extract-css'. + ${chalk.yellow('NOTICE')} Hot Module Replacement (HMR) is enabled for the dev server. `); + ui.writeLine(' The project will still live reload when HMR is enabled,'); + ui.writeLine(' but to take advantage of HMR additional application code is required'); + ui.writeLine(' (not included in an Angular CLI project by default).'); + ui.writeLine(` See ${chalk.blue(webpackHmrLink)}`); + ui.writeLine(' for information on working with HMR for Webpack.'); + entryPoints.push('webpack/hot/dev-server'); + webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); + webpackConfig.plugins.push(new webpack.NamedModulesPlugin()); + if (serveTaskOptions.extractCss) { + ui.writeLine(oneLine` + ${chalk.yellow('NOTICE')} (HMR) does not allow for CSS hot reload when used + together with '--extract-css'. + `); + } } + if (!webpackConfig.entry.main) { webpackConfig.entry.main = []; } + webpackConfig.entry.main.unshift(...entryPoints); + } else if (serveTaskOptions.hmr) { + ui.writeLine(chalk.yellow('Live reload is disabled. HMR option ignored.')); } - if (!webpackConfig.entry.main) { webpackConfig.entry.main = []; } - webpackConfig.entry.main.unshift(...entryPoints); if (!serveTaskOptions.watch) { // There's no option to turn off file watching in webpack-dev-server, but @@ -151,26 +170,26 @@ export default Task.extend({ ui.writeLine(chalk.green(oneLine` ** - NG Live Development Server is running on - http${serveTaskOptions.ssl ? 's' : ''}://${serveTaskOptions.host}:${serveTaskOptions.port}. + NG Live Development Server is running on ${serverAddress} ** `)); const server = new WebpackDevServer(webpackCompiler, webpackDevServerConfiguration); return new Promise((resolve, reject) => { - server.listen(serveTaskOptions.port, `${serveTaskOptions.host}`, (err: any, stats: any) => { + server.listen(serveTaskOptions.port, serveTaskOptions.host, (err: any, stats: any) => { if (err) { - console.error(err.stack || err); - if (err.details) { console.error(err.details); } - reject(err.details); - } else { - const { open, ssl, host, port } = serveTaskOptions; - if (open) { - let protocol = ssl ? 'https' : 'http'; - opn(url.format({ protocol: protocol, hostname: host, port: port.toString() })); - } + return reject(err); + } + if (serveTaskOptions.open) { + opn(serverAddress); } }); + }) + .catch((err: Error) => { + if (err) { + this.ui.writeError('\nAn error occured during the build:\n' + ((err && err.stack) || err)); + } + throw err; }); } }); diff --git a/tests/e2e/tests/misc/live-reload.ts b/tests/e2e/tests/misc/live-reload.ts new file mode 100644 index 000000000000..db36fca6016b --- /dev/null +++ b/tests/e2e/tests/misc/live-reload.ts @@ -0,0 +1,118 @@ +import * as express from 'express'; +import * as http from 'http'; + +import { appendToFile, writeMultipleFiles } from '../../utils/fs'; +import { + killAllProcesses, + silentExecAndWaitForOutputToMatch, + waitForAnyProcessOutputToMatch +} from '../../utils/process'; +import { wait } from '../../utils/utils'; + + +export default function () { + const protractorGoodRegEx = /Spec started/; + const webpackGoodRegEx = /webpack: Compiled successfully./; + + // Create an express api for the Angular app to call. + const app = express(); + const server = http.createServer(app); + let liveReloadCount = 0; + let liveReloadClientCalled = false; + function resetApiVars() { + liveReloadCount = 0; + liveReloadClientCalled = false; + } + + server.listen(0); + app.set('port', server.address().port); + const apiUrl = `http://localhost:${server.address().port}`; + + // This endpoint will be pinged by the main app on each reload. + app.get('/live-reload-count', _ => liveReloadCount++); + // This endpoint will be pinged by webpack to check for live reloads. + app.get('/sockjs-node/info', _ => liveReloadClientCalled = true); + + + return Promise.resolve() + .then(_ => writeMultipleFiles({ + // e2e test that just opens the page and waits, so that the app runs. + './e2e/app.e2e-spec.ts': ` + import { browser } from 'protractor'; + + describe('master-project App', function() { + it('should wait', _ => { + browser.get('/'); + browser.sleep(30000); + }); + }); + `, + // App that calls the express server once. + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { Http } from '@angular/http'; + + @Component({ + selector: 'app-root', + template: '

Live reload test

' + }) + export class AppComponent { + constructor(private http: Http) { + http.get('${apiUrl + '/live-reload-count'}').subscribe(res => null); + } + } + ` + })) + .then(_ => silentExecAndWaitForOutputToMatch( + 'ng', + ['e2e', '--watch', '--live-reload'], + protractorGoodRegEx + )) + // Let app run. + .then(_ => wait(1000)) + .then(_ => appendToFile('src/main.ts', 'console.log(1);')) + .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(_ => wait(1000)) + .then(_ => { + if (liveReloadCount != 2) { + throw new Error( + `Expected API to have been called 2 times but it was called ${liveReloadCount} times.` + ); + } + }) + .then(_ => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) + .then(_ => resetApiVars()) + // Serve with live reload off should call api only once. + .then(_ => silentExecAndWaitForOutputToMatch( + 'ng', + ['e2e', '--watch', '--no-live-reload'], + protractorGoodRegEx + )) + .then(_ => wait(1000)) + .then(_ => appendToFile('src/main.ts', 'console.log(1);')) + .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(_ => wait(1000)) + .then(_ => { + if (liveReloadCount != 1) { + throw new Error( + `Expected API to have been called 1 time but it was called ${liveReloadCount} times.` + ); + } + }) + .then(_ => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) + .then(_ => resetApiVars()) + // Serve with live reload client set to api should call api. + .then(_ => silentExecAndWaitForOutputToMatch( + 'ng', + ['e2e', '--watch', `--live-reload-client=${apiUrl}`], + protractorGoodRegEx + )) + .then(_ => wait(2000)) + .then(_ => { + if (!liveReloadClientCalled) { + throw new Error(`Expected live-reload client to have been called but it was not.`); + } + }) + .then(_ => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) + .then(_ => server.close(), (err) => { server.close(); throw err; }); +}