Components dev server
This introduces the components server that we will use to view
components locally.
Design doc:
https://docs.google.com/document/d/1P6qtACf4aryfT9OSHxNFI3okMKt9oUrtzOKCws5bOec/edit?pli=1
Change-Id: I6cd7588f045c3cd46e57f1f44441ffa95f0b70bb
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2199081
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
diff --git a/scripts/component_server/server.js b/scripts/component_server/server.js
new file mode 100644
index 0000000..7dfb0b9
--- /dev/null
+++ b/scripts/component_server/server.js
@@ -0,0 +1,165 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const fs = require('fs');
+const http = require('http');
+const path = require('path');
+const parseURL = require('url').parse;
+const {argv} = require('yargs');
+
+const serverPort = parseInt(process.env.PORT, 10) || 8090;
+
+const target = argv.target || 'Default';
+const devtoolsFrontendFolder = path.resolve(path.join(__dirname, '..', '..', 'out', target, 'gen', 'front_end'));
+
+if (!fs.existsSync(devtoolsFrontendFolder)) {
+ console.error(`ERROR: Generated front_end folder (${devtoolsFrontendFolder}) does not exist.`);
+ console.log(
+ 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
+ console.log('If you build to a target other than default, you need to pass --target=X as an argument');
+ process.exit(1);
+}
+
+http.createServer(requestHandler).listen(serverPort);
+console.log(`Started components server at http://localhost:${serverPort}\n`);
+
+function createComponentIndexFile(componentPath, componentExamples) {
+ const componentName = componentPath.replace('/', '');
+ // clang-format off
+ return `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>DevTools component: ${componentName}</title>
+ <style>
+ h1 { text-transform: capitalize; }
+
+ .example {
+ padding: 5px;
+ margin: 10px;
+ }
+ iframe { display: block; width: 100%; }
+ </style>
+ </head>
+ <body>
+ <h1>${componentName}</h1>
+ ${componentExamples.map(example => {
+ const fullPath = path.join('component_docs', componentName, example);
+ return `<div class="example">
+ <h3><a href="${fullPath}">${example}</a></h3>
+ <iframe src="${fullPath}"></iframe>
+ </div>`;
+ }).join('\n')}
+ </body>
+ </html>`;
+ // clang-format on
+}
+
+function createServerIndexFile(componentNames) {
+ // clang-format off
+ return `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>DevTools components</title>
+ <style>
+ a { text-transform: capitalize; }
+ </style>
+ </head>
+ <body>
+ <h1>DevTools components</h1>
+ <ul>
+ ${componentNames.map(name => {
+ return `<li><a href='/${name}'>${name}</a></li>`;
+ }).join('\n')}
+ </ul>
+ </body>
+ </html>`;
+ // clang-format on
+}
+
+async function getExamplesForPath(filePath) {
+ const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
+ const contents = await fs.promises.readdir(componentDirectory);
+
+ return createComponentIndexFile(filePath, contents);
+}
+
+function respondWithHtml(response, html) {
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
+ response.writeHead(200);
+ response.write(html, 'utf8');
+ response.end();
+}
+
+function send404(response, message) {
+ response.writeHead(404);
+ response.write(message, 'utf8');
+ response.end();
+}
+
+async function requestHandler(request, response) {
+ const filePath = parseURL(request.url).pathname;
+
+ if (filePath === '/favicon.ico') {
+ send404(response, '404, no favicon');
+ return;
+ }
+
+ if (filePath === '/' || filePath === '/index.html') {
+ const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
+ const html = createServerIndexFile(components);
+ respondWithHtml(response, html);
+ } else if (path.extname(filePath) === '') {
+ // This means it's a component path like /breadcrumbs.
+ const componentHtml = await getExamplesForPath(filePath);
+ respondWithHtml(response, componentHtml);
+ } else {
+ // This means it's an asset like a JS file.
+ const fullPath = path.join(devtoolsFrontendFolder, filePath);
+
+ if (!fullPath.startsWith(devtoolsFrontendFolder)) {
+ console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
+ process.exit(1);
+ }
+ const errorsAccesingFile = await fs.promises.access(fullPath, fs.constants.R_OK);
+
+ if (errorsAccesingFile) {
+ console.error(`File ${fullPath} does not exist.`);
+ send404(response, '404, File not found');
+ return;
+ }
+
+ let encoding = 'utf8';
+ if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
+ encoding = 'binary';
+ }
+
+ const fileContents = await fs.promises.readFile(fullPath, encoding);
+
+ encoding = 'utf8';
+ if (fullPath.endsWith('.js')) {
+ response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
+ } else if (fullPath.endsWith('.css')) {
+ response.setHeader('Content-Type', 'text/css; charset=utf-8');
+ } else if (fullPath.endsWith('.wasm')) {
+ response.setHeader('Content-Type', 'application/wasm');
+ encoding = 'binary';
+ } else if (fullPath.endsWith('.svg')) {
+ response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
+ } else if (fullPath.endsWith('.png')) {
+ response.setHeader('Content-Type', 'image/png');
+ encoding = 'binary';
+ } else if (fullPath.endsWith('.jpg')) {
+ response.setHeader('Content-Type', 'image/jpg');
+ encoding = 'binary';
+ }
+
+ response.writeHead(200);
+ response.write(fileContents, encoding);
+ response.end();
+ }
+}