preview/preview.js (174 lines of code) (raw):

/** * @license * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ 'use strict'; const http = require("http"); const path = require("path"); const url = require("url"); const Template = require("../template/template"); const port = 3000; const requestHandler = async (core, parsedUrl, response) => { if (parsedUrl.pathname === '/') { response.statusCode = 301; response.setHeader('Location', '/guide/index.html'); response.end(); return; } if (parsedUrl.pathname === '/diff') { return new Promise((resolve, reject) => { pipeToResponse(core.diff(), response, resolve, reject); }); } if (!parsedUrl.pathname.startsWith('/guide')) { const redirect = core.outsideOfGuide(parsedUrl.pathname); if (redirect) { response.statusCode = 302; response.setHeader('Location', redirect); response.end(); } else { response.statusCode = 404; response.end(); } return; } const path = parsedUrl.pathname.substring("/guide".length); const redirect = await checkRedirects(core, path); if (redirect) { response.statusCode = 301; response.setHeader('Location', redirect); response.end(); return; } const file = await core.file(path); if (file === "dir") { response.statusCode = 301; const sep = parsedUrl.pathname.endsWith('/') ? '' : '/'; response.setHeader('Location', `${parsedUrl.pathname}${sep}index.html`); response.end(); return; } if (file === "missing") { response.statusCode = 404; response.end(`Can't find ${parsedUrl.pathname}\n`); return; } const type = contentType(path); response.setHeader('Content-Type', type); if (file.hasTemplate && !path.endsWith("toc.html") && type === "text/html; charset=utf-8") { const template = Template(file.template); const lang = await file.lang(); const initialJsState = await buildInitialJsState(file.alternativesReport); const templated = template.apply( file.stream[Symbol.asyncIterator](), lang.trim(), initialJsState ); return new Promise((resolve, reject) => { file.stream.on("error", reject); pipeToResponse(templated, response, resolve, reject); }); } else { return new Promise((resolve, reject) => { pipeToResponse(file.stream, response, resolve, reject); }); } } const pipeToResponse = (out, response, resolve, reject) => { response.on("close", resolve); response.on("error", reject); out.on("error", reject); out.pipe(response); } const contentType = rawPath => { const ext = path.extname(rawPath); switch (ext) { case ".css": return "text/css"; case ".gif": return "image/gif"; case ".html": case ".htm": return "text/html; charset=utf-8"; case ".jpg": case ".jpeg": return "image/jpeg"; case ".js": return "application/javascript"; case ".svg": return "image/svg+xml"; default: return "text/plain"; } }; const buildInitialJsState = async alternativesReportSource => { try { const parsed = JSON.parse(await alternativesReportSource()); return JSON.stringify(Template.buildInitialJsState(parsed)); } catch (err) { if (err === "missing") { return "{}"; } throw err; } }; const hostPrefix = host => { if (!host) { return null; } const dot = host.indexOf("."); if (dot === -1) { return null; } return host.substring(0, dot); }; const checkRedirects = async (core, path) => { /* * This parses the nginx redirects.conf file we have in the built docs and * performs the redirects. It makes no effort to properly emulate nginx. It * just runs the regexes from start to finish. Which is fine becaues of the * redirects that we have. But it is ugly. * * It also doesen't make any effort to be fast or efficient, buffering the * entire file into memory then splitting it into lines and compiling all of * the regexes on the fly. We can absolutely do better. But this feels like * a fine place to start. */ // TODO Rebuild redirects file without nginx stuff. And stream it properly. let target = "/guide" + path; const streamToString = stream => { const chunks = [] return new Promise((resolve, reject) => { stream.on('data', chunk => chunks.push(chunk)); stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }); } const redirectsStream = await core.redirects(); if (!redirectsStream) { // If we don't have the redirects file we skip redirects. return; } const redirectsString = await streamToString(redirectsStream); for (const line of redirectsString.split('\n')) { if (!line.startsWith("rewrite")) { continue; } const [_marker, regexText, replacement] = line.split(' '); const regex = new RegExp(regexText.replace('(?i)', ''), 'i'); target = target.replace(regex, replacement); } return "/guide" + path === target ? null : target; } module.exports = Core => { const server = http.createServer((request, response) => { const parsedUrl = url.parse(request.url); const prefix = hostPrefix(request.headers['host']); const core = Core(prefix); requestHandler(core, parsedUrl, response) .catch(err => { if (err === "missing") { response.statusCode = 404; response.end("404!"); } else { console.warn('unhandled error for', prefix, parsedUrl, err); /* * *try* to set the status code to 500. This might not be possible * because we might be in the middle of a chunked transfer. In that * case this'll look funny. */ response.statusCode = 500; response.end(err.message); } }); }); server.listen(port, err => { if (err) { console.error("preview server couldn't listen", err); process.exit(1); } console.info(`preview server is listening on ${port}`); }); process.on('SIGTERM', () => { console.info('preview server shutting down'); server.close(() => { process.exit(); }); }); };