From 0bc68e5c80b4ce578251d3cecce7b6230da79427 Mon Sep 17 00:00:00 2001 From: rorticus Date: Fri, 31 Aug 2018 07:09:18 -0400 Subject: [PATCH] Adding HTTPS serve (#153) --- README.md | 23 +++++++++++---- src/main.ts | 43 +++++++++++++++++++++++----- tests/unit/main.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1684c003..1822444b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,17 @@ A web server can be started with the `--serve` flag while running in `dev` or `d dojo build -s -p 3000 ``` +By default, the files will be served via HTTP. HTTPS can be enabled by placing `server.crt` and `server.key` files in a `.cert` directory in the root of your project: + +```text +|-- my-project + |-- .cert + |-- .server.crt + |-- .server.key +``` + +When these files are detected, `dojo build -s` will automatically serve files via HTTPS. + ### Watching Building with the `--watch` option observes the file system for changes, and recompiles to the appropriate `output/{dist|dev|test}` directory, depending on the current `--mode`. When used in the conjunction with the `--serve` option and `--mode=dev`, `--watch=memory` can be specified to enable automatic browser updates and hot module replacement (HMR). @@ -148,12 +159,12 @@ Configuration for external dependencies can be provided under the `externals` pr * A string that indicates that this path, and any children of this path, should be loaded via the external loader. * An object that provides additional configuration for dependencies that need to be copied into the built application. This object has the following properties: - | Property | Type | optional | Description | - | -------- | ---- | -------- | ----------- | - | `from` | `string` | false | A path relative to the root of the project specifying the location of the files or folders to copy into the build application. | - | `to` | `string` | true | A path that replaces `from` as the location to copy this dependency to. By default, dependencies will be copied to `${externalsOutputPath}/${to}` or `${externalsOutputPath}/${from}` if `to` is not specified. If there are any `.` characters in the path and it is a directory, it needs to end with a forward slash. | - | `name` | `string` | true | Either the module id or the name of the global variable referenced in the application source. | - | `inject` | `string, string[], or boolean` | true | This property indicates that this dependency defines, or includes, scripts or stylesheets that should be loaded on the page. If `inject` is set to `true`, then the file at the location specified by `to` or `from` will be loaded on the page. If this dependency is a folder, then `inject` can be set to a string or array of strings to define one or more files to inject. Each path in `inject` should be relative to `${externalsOutputPath}/${to}` or `${externalsOutputPath}/${from}` depending on whether `to` was provided. | +| Property | Type | optional | Description | +| -------- | ---- | -------- | ----------- | +| `from` | `string` | false | A path relative to the root of the project specifying the location of the files or folders to copy into the build application. | +| `to` | `string` | true | A path that replaces `from` as the location to copy this dependency to. By default, dependencies will be copied to `${externalsOutputPath}/${to}` or `${externalsOutputPath}/${from}` if `to` is not specified. If there are any `.` characters in the path and it is a directory, it needs to end with a forward slash. | +| `name` | `string` | true | Either the module id or the name of the global variable referenced in the application source. | +| `inject` | `string, string[], or boolean` | true | This property indicates that this dependency defines, or includes, scripts or stylesheets that should be loaded on the page. If `inject` is set to `true`, then the file at the location specified by `to` or `from` will be loaded on the page. If this dependency is a folder, then `inject` can be set to a string or array of strings to define one or more files to inject. Each path in `inject` should be relative to `${externalsOutputPath}/${to}` or `${externalsOutputPath}/${from}` depending on whether `to` was provided. | As an example the following configuration will inject `src/legacy/layer.js` into the application page, inject the file that defines the `MyGlobal` global variable, declare that modules `a` and `b` are external and should be delegated to the external layer, and then copy the folder `node_modules/legacy-dep`, from which several files are injected. All of these files will be copied into the `externals` folder, which could be overridden by specifying the `outputPath` property in the `externals` configuration. diff --git a/src/main.ts b/src/main.ts index f0831ba1..1144254a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,8 @@ import * as ora from 'ora'; import * as path from 'path'; import * as webpack from 'webpack'; import chalk from 'chalk'; +import * as fs from 'fs'; +import * as https from 'https'; const pkgDir = require('pkg-dir'); import devConfigFactory from './dev.config'; @@ -125,6 +127,8 @@ function memoryWatch(config: webpack.Configuration, args: any, app: express.Appl } function serve(config: webpack.Configuration, args: any): Promise { + let isHttps = false; + const app = express(); if (args.watch !== 'memory') { @@ -132,6 +136,13 @@ function serve(config: webpack.Configuration, args: any): Promise { app.use(express.static(outputDir)); } + const defaultKey = path.resolve('.cert', 'server.key'); + const defaultCrt = path.resolve('.cert', 'server.crt'); + + if (fs.existsSync(defaultKey) && fs.existsSync(defaultCrt)) { + isHttps = true; + } + return Promise.resolve() .then(() => { if (args.watch === 'memory' && args.mode === 'dev') { @@ -149,13 +160,31 @@ function serve(config: webpack.Configuration, args: any): Promise { }) .then(() => { return new Promise((resolve, reject) => { - app.listen(args.port, (error: Error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); + if (isHttps) { + https + .createServer( + { + key: fs.readFileSync(defaultKey), + cert: fs.readFileSync(defaultCrt) + }, + app + ) + .listen(args.port, (error: Error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + } else { + app.listen(args.port, (error: Error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + } }); }); } diff --git a/tests/unit/main.ts b/tests/unit/main.ts index 889b33e2..001fcc76 100644 --- a/tests/unit/main.ts +++ b/tests/unit/main.ts @@ -4,6 +4,7 @@ import { join } from 'path'; import { SinonStub, stub } from 'sinon'; import chalk from 'chalk'; import MockModule from '../support/MockModule'; +import * as fs from 'fs'; let mockModule: MockModule; let mockLogger: any; @@ -43,6 +44,7 @@ describe('command', () => { './dev.config', './dist.config', './test.config', + 'https', 'express', 'log-update', 'ora', @@ -437,6 +439,75 @@ describe('command', () => { ); }); }); + + describe('https', () => { + it('starts an https server if key and cert are available', () => { + const main = mockModule.getModuleUnderTest().default; + + const listenStub = stub().callsFake((port: string, callback: Function) => { + callback(false); + }); + const createServerStub = mockModule.getMock('https').createServer; + createServerStub.returns({ + listen: listenStub + }); + + const existsStub = stub(fs, 'existsSync'); + existsStub.returns(true); + const readStub = stub(fs, 'readFileSync'); + readStub.returns('data'); + + return main + .run(getMockConfiguration(), { + serve: true + }) + .then(() => { + assert.isTrue( + createServerStub.calledWith({ + cert: 'data', + key: 'data' + }) + ); + existsStub.restore(); + readStub.restore(); + }) + .catch((e: any) => { + existsStub.restore(); + readStub.restore(); + throw e; + }); + }); + + it('throws https server errors', () => { + const main = mockModule.getModuleUnderTest().default; + + const listenStub = stub().callsFake((port: string, callback: Function) => { + callback('there is an error'); + }); + const createServerStub = mockModule.getMock('https').createServer; + createServerStub.returns({ + listen: listenStub + }); + + const existsStub = stub(fs, 'existsSync'); + existsStub.returns(true); + const readStub = stub(fs, 'readFileSync'); + readStub.returns('data'); + + return main + .run(getMockConfiguration(), { + serve: true + }) + .then(() => { + throw new Error('should not resolve'); + }) + .catch((e: any) => { + existsStub.restore(); + readStub.restore(); + assert.strictEqual(e, 'there is an error'); + }); + }); + }); }); describe('eject', () => {