| // 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('/', '').replace(/_/g, ' '); |
| // 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', componentPath, 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 checkFileExists(filePath) { |
| try { |
| const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK); |
| return !errorsAccessingFile; |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| 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 fileExists = await checkFileExists(fullPath); |
| |
| if (!fileExists) { |
| 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(); |
| } |
| } |