UNPKG

16.8 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt
7 * The complete set of authors may be found at
8 * http://polymer.github.io/AUTHORS.txt
9 * The complete set of contributors may be found at
10 * http://polymer.github.io/CONTRIBUTORS.txt
11 * Code distributed by Google as part of the polymer project is also
12 * subject to an additional IP rights grant found at
13 * http://polymer.github.io/PATENTS.txt
14 */
15var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16 return new (P || (P = Promise))(function (resolve, reject) {
17 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
20 step((generator = generator.apply(thisArg, _arguments || [])).next());
21 });
22};
23Object.defineProperty(exports, "__esModule", { value: true });
24const assert = require("assert");
25const escapeHtml = require("escape-html");
26const express = require("express");
27const fs = require("mz/fs");
28const path = require("path");
29const path_transformers_1 = require("polymer-build/lib/path-transformers");
30const send = require("send");
31// TODO: Switch to node-http2 when compatible with express
32// https://github.com/molnarg/node-http2/issues/100
33const http = require("spdy");
34const compile_middleware_1 = require("./compile-middleware");
35const config_1 = require("./config");
36const custom_elements_es5_adapter_middleware_1 = require("./custom-elements-es5-adapter-middleware");
37const make_app_1 = require("./make_app");
38const open_browser_1 = require("./util/open_browser");
39const push_1 = require("./util/push");
40const tls_1 = require("./util/tls");
41const compression = require("compression");
42const cors = require("cors");
43const httpProxy = require('http-proxy-middleware');
44function applyDefaultServerOptions(options) {
45 const withDefaults = Object.assign({}, options);
46 Object.assign(withDefaults, {
47 port: options.port || 0,
48 hostname: options.hostname || 'localhost',
49 root: path.resolve(options.root || '.'),
50 compile: options.compile || 'auto',
51 certPath: options.certPath || 'cert.pem',
52 keyPath: options.keyPath || 'key.pem',
53 componentDir: config_1.getComponentDir(options),
54 componentUrl: options.componentUrl || 'components'
55 });
56 withDefaults.packageName = config_1.getPackageName(withDefaults);
57 return withDefaults;
58}
59/**
60 * @param {ServerOptions} options used to configure the generated polyserve app
61 * and server.
62 * @param {ExpressAppMapper} appMapper optional mapper function which is called
63 * with the generated polyserve app and the options used to generate
64 * it and returns an optional substitution Express app. This is usually one
65 * that mounts the original app, to add routes or middleware in advance of
66 * polyserve's catch-all routes.
67 * @return {Promise} A Promise that completes when the server has started.
68 * @deprecated Please use `startServers` instead. This function will be removed
69 * in a future release.
70 */
71function startServer(options, appMapper) {
72 return __awaiter(this, void 0, void 0, function* () {
73 return (yield _startServer(options, appMapper)).server;
74 });
75}
76exports.startServer = startServer;
77function _startServer(options, appMapper) {
78 return __awaiter(this, void 0, void 0, function* () {
79 options = options || {};
80 assertNodeVersion(options);
81 try {
82 let app = getApp(options);
83 if (appMapper) {
84 // If the map function doesn't return an app, we should fallback to the
85 // original app, hence the `appMapper(app) || app`.
86 app = (yield appMapper(app, options)) || app;
87 }
88 const server = yield startWithApp(options, app);
89 return { app, server };
90 }
91 catch (e) {
92 console.error('ERROR: Server failed to start:', e);
93 throw new Error(e);
94 }
95 });
96}
97/**
98 * Starts one or more web servers, based on the given options and
99 * variant bower_components directories that are found in the root dir.
100 */
101function startServers(options, appMapper) {
102 return __awaiter(this, void 0, void 0, function* () {
103 options = applyDefaultServerOptions(options);
104 const variants = yield findVariants(options);
105 // TODO(rictic): support manually configuring variants? tracking more
106 // metadata about them besides their names?
107 if (variants.length > 0) {
108 return yield startVariants(options, variants, appMapper);
109 }
110 const serverAndApp = yield _startServer(options, appMapper);
111 return {
112 options,
113 kind: 'mainline',
114 server: serverAndApp.server,
115 app: serverAndApp.app,
116 };
117 });
118}
119exports.startServers = startServers;
120// TODO(usergenic): Variants should support the directory naming convention in
121// the .bowerrc instead of hardcoded 'bower_components' form seen here.
122function findVariants(options) {
123 return __awaiter(this, void 0, void 0, function* () {
124 const root = options.root || process.cwd();
125 const filesInRoot = yield fs.readdir(root);
126 const variants = filesInRoot
127 .map((f) => {
128 const match = f.match(`^${options.componentDir}-(.*)`);
129 return match && { name: match[1], directory: match[0] };
130 })
131 .filter((f) => f != null && f.name !== '');
132 return variants;
133 });
134}
135function startVariants(options, variants, appMapper) {
136 return __awaiter(this, void 0, void 0, function* () {
137 const mainlineOptions = Object.assign({}, options);
138 mainlineOptions.port = 0;
139 const mainServer = yield _startServer(mainlineOptions, appMapper);
140 const mainServerInfo = {
141 kind: 'mainline',
142 server: mainServer.server,
143 app: mainServer.app,
144 options: mainlineOptions,
145 };
146 const variantServerInfos = [];
147 for (const variant of variants) {
148 const variantOpts = Object.assign({}, options);
149 variantOpts.port = 0;
150 variantOpts.componentDir = variant.directory;
151 const variantServer = yield _startServer(variantOpts, appMapper);
152 variantServerInfos.push({
153 kind: 'variant',
154 variantName: variant.name,
155 dependencyDir: variant.directory,
156 server: variantServer.server,
157 app: variantServer.app,
158 options: variantOpts
159 });
160 }
161 const controlServerInfo = yield startControlServer(options, mainServerInfo, variantServerInfos);
162 const servers = [controlServerInfo, mainServerInfo]
163 .concat(variantServerInfos);
164 const result = {
165 kind: 'MultipleServers',
166 control: controlServerInfo,
167 mainline: mainServerInfo,
168 variants: variantServerInfos,
169 servers,
170 };
171 return result;
172 });
173}
174function startControlServer(options, mainlineInfo, variantInfos) {
175 return __awaiter(this, void 0, void 0, function* () {
176 options = applyDefaultServerOptions(options);
177 const app = express();
178 app.get('/api/serverInfo', (_req, res) => {
179 res.contentType('json');
180 res.send(JSON.stringify({
181 packageName: options.packageName,
182 mainlineServer: {
183 port: assertNotString(mainlineInfo.server.address()).port,
184 },
185 variants: variantInfos.map((info) => {
186 return {
187 name: info.variantName,
188 port: assertNotString(info.server.address()).port,
189 };
190 })
191 }));
192 res.end();
193 });
194 const indexPath = path.join(__dirname, '..', 'static', 'index.html');
195 app.get('/', (_req, res) => __awaiter(this, void 0, void 0, function* () {
196 res.contentType('html');
197 const indexContents = yield fs.readFile(indexPath, 'utf-8');
198 res.send(indexContents);
199 res.end();
200 }));
201 const controlServer = {
202 kind: 'control',
203 options: options,
204 server: yield startWithApp(options, app),
205 app
206 };
207 return controlServer;
208 });
209}
210exports.startControlServer = startControlServer;
211function getApp(options) {
212 options = applyDefaultServerOptions(options);
213 // Preload the h2-push manifest to avoid the cost on first push
214 if (options.pushManifestPath) {
215 push_1.getPushManifest(options.root, options.pushManifestPath);
216 }
217 const root = options.root || '.';
218 const app = express();
219 app.use(compression());
220 if (options.additionalRoutes) {
221 options.additionalRoutes.forEach((handler, route) => {
222 app.get(route, handler);
223 });
224 }
225 const componentUrl = options.componentUrl;
226 const polyserve = make_app_1.makeApp({
227 componentDir: options.componentDir,
228 packageName: options.packageName,
229 root: root,
230 headers: options.headers,
231 });
232 const filePathRegex = /.*\/.+\..{1,}$/;
233 if (options.proxy) {
234 if (options.proxy.path.startsWith(componentUrl)) {
235 console.error(`proxy path can not start with ${componentUrl}.`);
236 return;
237 }
238 let escapedPath = options.proxy.path;
239 for (const char of ['*', '?', '+']) {
240 if (escapedPath.indexOf(char) > -1) {
241 console.warn(`Proxy path includes character "${char}"` +
242 `which can cause problems during route matching.`);
243 }
244 }
245 if (escapedPath.startsWith('/')) {
246 escapedPath = escapedPath.substring(1);
247 }
248 if (escapedPath.endsWith('/')) {
249 escapedPath = escapedPath.slice(0, -1);
250 }
251 const pathRewrite = {};
252 pathRewrite[`^/${escapedPath}`] = '';
253 const apiProxy = httpProxy(`/${escapedPath}`, {
254 target: options.proxy.target,
255 changeOrigin: true,
256 pathRewrite: pathRewrite,
257 logLevel: 'warn',
258 });
259 app.use(`/${escapedPath}/`, apiProxy);
260 }
261 app.use('*', custom_elements_es5_adapter_middleware_1.injectCustomElementsEs5Adapter(options.compile));
262 app.use('*', compile_middleware_1.babelCompile(options.compile, options.moduleResolution, root, options.packageName, options.componentUrl, options.componentDir));
263 if (options.allowOrigin) {
264 app.use(cors({ origin: options.allowOrigin }));
265 }
266 app.use(`/${componentUrl}/`, polyserve);
267 // `send` expects files to be specified relative to the given root and as a
268 // URL rather than a file system path.
269 const entrypoint = options.entrypoint ?
270 path_transformers_1.urlFromPath(root, options.entrypoint) :
271 'index.html';
272 app.get('/*', (req, res) => {
273 push_1.pushResources(options, req, res);
274 const filePath = req.path;
275 send(req, filePath, { root: root, index: entrypoint, etag: false, lastModified: false })
276 .on('error', (error) => {
277 if (error.status === 404 && !filePathRegex.test(filePath)) {
278 // The static file handling middleware failed to find a file on
279 // disk. Serve the entry point HTML file instead of a 404.
280 send(req, entrypoint, { root: root }).pipe(res);
281 }
282 else {
283 res.status(error.status || 500);
284 res.type('html');
285 res.end(escapeHtml(error.message));
286 }
287 })
288 .pipe(res);
289 });
290 return app;
291}
292exports.getApp = getApp;
293/**
294 * Determines whether a protocol requires HTTPS
295 * @param {string} protocol Protocol to evaluate.
296 * @returns {boolean}
297 */
298function isHttps(protocol) {
299 return ['https/1.1', 'https', 'h2'].indexOf(protocol) > -1;
300}
301/**
302 * Gets the URLs for the main and component pages
303 * @param {ServerOptions} options
304 * @returns {{serverUrl: {protocol: string, hostname: string, port: string},
305 * componentUrl: url.Url}}
306 */
307function getServerUrls(options, server) {
308 options = applyDefaultServerOptions(options);
309 const address = assertNotString(server.address());
310 const serverUrl = {
311 protocol: isHttps(options.protocol) ? 'https' : 'http',
312 hostname: address.address,
313 port: String(address.port),
314 };
315 const componentUrl = Object.assign({}, serverUrl);
316 componentUrl.pathname = `${options.componentUrl}/${options.packageName}/`;
317 return { serverUrl, componentUrl };
318}
319exports.getServerUrls = getServerUrls;
320/**
321 * Asserts that Node version is valid for h2 protocol
322 * @param {ServerOptions} options
323 */
324function assertNodeVersion(options) {
325 if (options.protocol === 'h2') {
326 const matches = /(\d+)\./.exec(process.version);
327 if (matches) {
328 const major = Number(matches[1]);
329 assert(major >= 5, 'h2 requires ALPN which is only supported in node.js >= 5.0');
330 }
331 }
332}
333/**
334 * Creates an HTTP(S) server
335 * @param app
336 * @param {ServerOptions} options
337 * @returns {Promise<http.Server>} Promise of server
338 */
339function createServer(app, options) {
340 return __awaiter(this, void 0, void 0, function* () {
341 // tslint:disable-next-line: no-any bad typings
342 const opt = { spdy: { protocols: [options.protocol] } };
343 if (isHttps(options.protocol)) {
344 const keys = yield tls_1.getTLSCertificate(options.keyPath, options.certPath);
345 opt.key = keys.key;
346 opt.cert = keys.cert;
347 }
348 else {
349 opt.spdy.plain = true;
350 opt.spdy.ssl = false;
351 }
352 return http.createServer(opt, app);
353 });
354}
355// Sauce Labs compatible ports taken from
356// https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+FAQS#SauceConnectProxyFAQS-CanIAccessApplicationsonlocalhost?
357// - 80, 443, 888: these ports must have root access
358// - 5555, 8080: not forwarded on Android
359const SAUCE_PORTS = [
360 8081, 8000, 8001, 8003, 8031,
361 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333,
362 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5432,
363 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8765, 8777,
364 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221, 55001
365];
366/**
367 * Starts an HTTP(S) server serving the given app.
368 */
369function startWithApp(options, app) {
370 return __awaiter(this, void 0, void 0, function* () {
371 options = applyDefaultServerOptions(options);
372 const ports = options.port ? [options.port] : SAUCE_PORTS;
373 const server = yield startWithFirstAvailablePort(options, app, ports);
374 const urls = getServerUrls(options, server);
375 open_browser_1.openBrowser(options, urls.serverUrl, urls.componentUrl);
376 return server;
377 });
378}
379exports.startWithApp = startWithApp;
380function startWithFirstAvailablePort(options, app, ports) {
381 return __awaiter(this, void 0, void 0, function* () {
382 for (const port of ports) {
383 const server = yield tryStartWithPort(options, app, port);
384 if (server) {
385 return server;
386 }
387 }
388 throw new Error(`No available ports. Ports tried: ${JSON.stringify(ports)}`);
389 });
390}
391function tryStartWithPort(options, app, port) {
392 return __awaiter(this, void 0, void 0, function* () {
393 const server = yield createServer(app, options);
394 return new Promise((resolve, _reject) => {
395 server.listen(port, options.hostname, () => {
396 resolve(server);
397 });
398 server.on('error', (_err) => {
399 resolve(null);
400 });
401 });
402 });
403}
404// TODO(usergenic): Something changed in the typings of net.Server.address() in
405// that it can now return AddressInfo OR string. I don't know the circumstances
406// where the the address() returns a string or how to handle it, so I made this
407// assert function when calling on the address to fix compilation errors and
408// have a runtime error as soon as the address is fetched.
409function assertNotString(value) {
410 assert(typeof value !== 'string');
411 return value;
412}
413exports.assertNotString = assertNotString;
414//# sourceMappingURL=start_server.js.map
\No newline at end of file