polyserve
Version:
A simple dev server for bower components
388 lines • 15.4 kB
JavaScript
;
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
const express = require("express");
const fs = require("mz/fs");
const path = require("path");
const path_transformers_1 = require("polymer-build/lib/path-transformers");
const send = require("send");
// TODO: Switch to node-http2 when compatible with express
// https://github.com/molnarg/node-http2/issues/100
const http = require("spdy");
const bower_config_1 = require("./bower_config");
const compile_middleware_1 = require("./compile-middleware");
const custom_elements_es5_adapter_middleware_1 = require("./custom-elements-es5-adapter-middleware");
const make_app_1 = require("./make_app");
const open_browser_1 = require("./util/open_browser");
const push_1 = require("./util/push");
const tls_1 = require("./util/tls");
const compression = require("compression");
const httpProxy = require('http-proxy-middleware');
function applyDefaultServerOptions(options) {
const withDefaults = Object.assign({}, options);
Object.assign(withDefaults, {
port: options.port || 0,
hostname: options.hostname || 'localhost',
root: path.resolve(options.root || '.'),
compile: options.compile || 'auto',
certPath: options.certPath || 'cert.pem',
keyPath: options.keyPath || 'key.pem',
// TODO(usergenic): The current behavior of polyserve is to use
// bower_components for directory and components for url. We should
// honor the value of `directory` of `.bowerrc` if found in the root dir
// of the app, to get user-defined defaults.
componentDir: options.componentDir || 'bower_components',
componentUrl: options.componentUrl || 'components'
});
withDefaults.packageName = options.packageName ||
bower_config_1.bowerConfig(withDefaults.root).name || path.basename(process.cwd());
return withDefaults;
}
/**
* @return {Promise} A Promise that completes when the server has started.
* @deprecated Please use `startServers` instead. This function will be removed
* in a future release.
*/
function startServer(options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield _startServer(options)).server;
});
}
exports.startServer = startServer;
function _startServer(options) {
return __awaiter(this, void 0, void 0, function* () {
options = options || {};
assertNodeVersion(options);
try {
const app = getApp(options);
const server = yield startWithApp(options, app);
return { app, server };
}
catch (e) {
console.error('ERROR: Server failed to start:', e);
throw new Error(e);
}
});
}
/**
* Starts one or more web servers, based on the given options and
* variant bower_components directories that are found in the root dir.
*/
function startServers(options) {
return __awaiter(this, void 0, void 0, function* () {
options = applyDefaultServerOptions(options);
const variants = yield findVariants(options);
// TODO(rictic): support manually configuring variants? tracking more
// metadata about them besides their names?
if (variants.length > 0) {
return yield startVariants(options, variants);
}
const serverAndApp = yield _startServer(options);
return {
options,
kind: 'mainline',
server: serverAndApp.server,
app: serverAndApp.app,
};
});
}
exports.startServers = startServers;
// TODO(usergenic): Variants should support the directory naming convention in
// the .bowerrc instead of hardcoded 'bower_components' form seen here.
function findVariants(options) {
return __awaiter(this, void 0, void 0, function* () {
const root = options.root || process.cwd();
const filesInRoot = yield fs.readdir(root);
const variants = filesInRoot
.map(f => {
const match = f.match(/^bower_components-(.*)/);
return match && { name: match[1], directory: match[0] };
})
.filter(f => f != null && f.name !== '');
return variants;
});
}
function startVariants(options, variants) {
return __awaiter(this, void 0, void 0, function* () {
const mainlineOptions = Object.assign({}, options);
mainlineOptions.port = 0;
const mainServer = yield _startServer(mainlineOptions);
const mainServerInfo = {
kind: 'mainline',
server: mainServer.server,
app: mainServer.app,
options: mainlineOptions,
};
const variantServerInfos = [];
for (const variant of variants) {
const variantOpts = Object.assign({}, options);
variantOpts.port = 0;
variantOpts.componentDir = variant.directory;
const variantServer = yield _startServer(variantOpts);
variantServerInfos.push({
kind: 'variant',
variantName: variant.name,
dependencyDir: variant.directory,
server: variantServer.server,
app: variantServer.app,
options: variantOpts
});
}
;
const controlServerInfo = yield startControlServer(options, mainServerInfo, variantServerInfos);
const servers = [controlServerInfo, mainServerInfo]
.concat(variantServerInfos);
const result = {
kind: 'MultipleServers',
control: controlServerInfo,
mainline: mainServerInfo,
variants: variantServerInfos, servers,
};
return result;
});
}
;
function startControlServer(options, mainlineInfo, variantInfos) {
return __awaiter(this, void 0, void 0, function* () {
options = applyDefaultServerOptions(options);
const app = express();
app.get('/api/serverInfo', (_req, res) => {
res.contentType('json');
res.send(JSON.stringify({
packageName: options.packageName,
mainlineServer: {
port: mainlineInfo.server.address().port,
},
variants: variantInfos.map(info => {
return { name: info.variantName, port: info.server.address().port };
})
}));
res.end();
});
const indexPath = path.join(__dirname, '..', 'static', 'index.html');
app.get('/', (_req, res) => __awaiter(this, void 0, void 0, function* () {
res.contentType('html');
const indexContents = yield fs.readFile(indexPath, 'utf-8');
res.send(indexContents);
res.end();
}));
const controlServer = {
kind: 'control',
options: options,
server: yield startWithApp(options, app), app
};
return controlServer;
});
}
exports.startControlServer = startControlServer;
function getApp(options) {
options = applyDefaultServerOptions(options);
// Preload the h2-push manifest to avoid the cost on first push
if (options.pushManifestPath) {
push_1.getPushManifest(options.root, options.pushManifestPath);
}
const root = options.root || '.';
const app = express();
app.use(compression());
if (options.additionalRoutes) {
options.additionalRoutes.forEach((handler, route) => {
app.get(route, handler);
});
}
const componentUrl = options.componentUrl;
const polyserve = make_app_1.makeApp({
componentDir: options.componentDir,
packageName: options.packageName,
root: root,
headers: options.headers,
});
options.packageName = polyserve.packageName;
const filePathRegex = /.*\/.+\..{1,}$/;
if (options.proxy) {
if (options.proxy.path.startsWith(componentUrl)) {
console.error(`proxy path can not start with ${componentUrl}.`);
return;
}
let escapedPath = options.proxy.path;
for (let char of ['*', '?', '+']) {
if (escapedPath.indexOf(char) > -1) {
console.warn(`Proxy path includes character "${char}"` +
`which can cause problems during route matching.`);
}
}
if (escapedPath.startsWith('/')) {
escapedPath = escapedPath.substring(1);
}
if (escapedPath.endsWith('/')) {
escapedPath = escapedPath.slice(0, -1);
}
const pathRewrite = {};
pathRewrite[`^/${escapedPath}`] = '';
const apiProxy = httpProxy(`/${escapedPath}`, {
target: options.proxy.target,
changeOrigin: true,
pathRewrite: pathRewrite
});
app.use(`/${escapedPath}/`, apiProxy);
}
const forceCompile = options.compile === 'always';
if (options.compile === 'auto' || forceCompile) {
app.use('*', custom_elements_es5_adapter_middleware_1.injectCustomElementsEs5Adapter(forceCompile));
app.use('*', compile_middleware_1.babelCompile(forceCompile));
}
app.use(`/${componentUrl}/`, polyserve);
// `send` expects files to be specified relative to the given root and as a
// URL rather than a file system path.
const entrypoint = options.entrypoint ? path_transformers_1.urlFromPath(root, options.entrypoint) : 'index.html';
app.get('/*', (req, res) => {
push_1.pushResources(options, req, res);
const filePath = req.path;
send(req, filePath, { root: root, index: entrypoint })
.on('error', (error) => {
if ((error).status === 404 && !filePathRegex.test(filePath)) {
// The static file handling middleware failed to find a file on
// disk. Serve the entry point HTML file instead of a 404.
send(req, entrypoint, { root: root }).pipe(res);
}
else {
res.statusCode = error.status || 500;
res.end(error.message);
}
})
.pipe(res);
});
return app;
}
exports.getApp = getApp;
/**
* Determines whether a protocol requires HTTPS
* @param {string} protocol Protocol to evaluate.
* @returns {boolean}
*/
function isHttps(protocol) {
return ['https/1.1', 'https', 'h2'].indexOf(protocol) > -1;
}
/**
* Gets the URLs for the main and component pages
* @param {ServerOptions} options
* @returns {{serverUrl: {protocol: string, hostname: string, port: string},
* componentUrl: url.Url}}
*/
function getServerUrls(options, server) {
options = applyDefaultServerOptions(options);
const address = server.address();
const serverUrl = {
protocol: isHttps(options.protocol) ? 'https' : 'http',
hostname: address.address,
port: String(address.port),
};
const componentUrl = Object.assign({}, serverUrl);
componentUrl.pathname = `${options.componentUrl}/${options.packageName}/`;
return { serverUrl, componentUrl };
}
exports.getServerUrls = getServerUrls;
/**
* Asserts that Node version is valid for h2 protocol
* @param {ServerOptions} options
*/
function assertNodeVersion(options) {
if (options.protocol === 'h2') {
const matches = /(\d+)\./.exec(process.version);
if (matches) {
const major = Number(matches[1]);
assert(major >= 5, 'h2 requires ALPN which is only supported in node.js >= 5.0');
}
}
}
/**
* Creates an HTTP(S) server
* @param app
* @param {ServerOptions} options
* @returns {Promise<http.Server>} Promise of server
*/
function createServer(app, options) {
return __awaiter(this, void 0, void 0, function* () {
const opt = { spdy: { protocols: [options.protocol] } };
if (isHttps(options.protocol)) {
const keys = yield tls_1.getTLSCertificate(options.keyPath, options.certPath);
opt.key = keys.key;
opt.cert = keys.cert;
}
else {
opt.spdy.plain = true;
opt.spdy.ssl = false;
}
return http.createServer(opt, app);
});
}
// Sauce Labs compatible ports taken from
// https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+FAQS#SauceConnectProxyFAQS-CanIAccessApplicationsonlocalhost?
// - 80, 443, 888: these ports must have root access
// - 5555, 8080: not forwarded on Android
const SAUCE_PORTS = [
8081, 8000, 8001, 8003, 8031,
2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333,
4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5432,
6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8765, 8777,
8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221, 55001
];
/**
* Starts an HTTP(S) server serving the given app.
*/
function startWithApp(options, app) {
return __awaiter(this, void 0, void 0, function* () {
options = applyDefaultServerOptions(options);
const ports = options.port ? [options.port] : SAUCE_PORTS;
const server = yield startWithFirstAvailablePort(options, app, ports);
const urls = getServerUrls(options, server);
open_browser_1.openBrowser(options, urls.serverUrl, urls.componentUrl);
return server;
});
}
exports.startWithApp = startWithApp;
function startWithFirstAvailablePort(options, app, ports) {
return __awaiter(this, void 0, void 0, function* () {
for (const port of ports) {
const server = yield tryStartWithPort(options, app, port);
if (server) {
return server;
}
}
throw new Error(`No available ports. Ports tried: ${JSON.stringify(ports)}`);
});
}
function tryStartWithPort(options, app, port) {
return __awaiter(this, void 0, void 0, function* () {
const server = yield createServer(app, options);
return new Promise((resolve, _reject) => {
server.listen(port, options.hostname, () => {
resolve(server);
});
server.on('error', (_err) => {
resolve(null);
});
});
});
}
//# sourceMappingURL=start_server.js.map