blob: 72870ce548c5b67008026eb180a4b825cddfb445 [file] [log] [blame]
Jack Franklin1557a1c2020-06-08 15:22:13 +01001// Copyright (c) 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5const fs = require('fs');
6const http = require('http');
7const path = require('path');
8const parseURL = require('url').parse;
9const {argv} = require('yargs');
10
11const serverPort = parseInt(process.env.PORT, 10) || 8090;
12
13const target = argv.target || 'Default';
14const devtoolsFrontendFolder = path.resolve(path.join(__dirname, '..', '..', 'out', target, 'gen', 'front_end'));
15
16if (!fs.existsSync(devtoolsFrontendFolder)) {
17 console.error(`ERROR: Generated front_end folder (${devtoolsFrontendFolder}) does not exist.`);
18 console.log(
19 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
20 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
21 process.exit(1);
22}
23
24http.createServer(requestHandler).listen(serverPort);
25console.log(`Started components server at http://localhost:${serverPort}\n`);
26
27function createComponentIndexFile(componentPath, componentExamples) {
Changhao Hand2454322020-08-11 15:09:35 +020028 const componentName = componentPath.replace('/', '').replace(/_/g, ' ');
Jack Franklin1557a1c2020-06-08 15:22:13 +010029 // clang-format off
30 return `<!DOCTYPE html>
31 <html>
32 <head>
33 <meta charset="UTF-8" />
34 <meta name="viewport" content="width=device-width" />
35 <title>DevTools component: ${componentName}</title>
36 <style>
37 h1 { text-transform: capitalize; }
38
39 .example {
40 padding: 5px;
41 margin: 10px;
42 }
43 iframe { display: block; width: 100%; }
44 </style>
45 </head>
46 <body>
47 <h1>${componentName}</h1>
48 ${componentExamples.map(example => {
Jack Franklin279564e2020-07-06 15:25:18 +010049 const fullPath = path.join('component_docs', componentPath, example);
Jack Franklin1557a1c2020-06-08 15:22:13 +010050 return `<div class="example">
51 <h3><a href="${fullPath}">${example}</a></h3>
52 <iframe src="${fullPath}"></iframe>
53 </div>`;
54 }).join('\n')}
55 </body>
56 </html>`;
57 // clang-format on
58}
59
60function createServerIndexFile(componentNames) {
61 // clang-format off
62 return `<!DOCTYPE html>
63 <html>
64 <head>
65 <meta charset="UTF-8" />
66 <meta name="viewport" content="width=device-width" />
67 <title>DevTools components</title>
68 <style>
69 a { text-transform: capitalize; }
70 </style>
71 </head>
72 <body>
73 <h1>DevTools components</h1>
74 <ul>
75 ${componentNames.map(name => {
76 return `<li><a href='/${name}'>${name}</a></li>`;
77 }).join('\n')}
78 </ul>
79 </body>
80 </html>`;
81 // clang-format on
82}
83
84async function getExamplesForPath(filePath) {
85 const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
86 const contents = await fs.promises.readdir(componentDirectory);
87
88 return createComponentIndexFile(filePath, contents);
89}
90
91function respondWithHtml(response, html) {
92 response.setHeader('Content-Type', 'text/html; charset=utf-8');
93 response.writeHead(200);
94 response.write(html, 'utf8');
95 response.end();
96}
97
98function send404(response, message) {
99 response.writeHead(404);
100 response.write(message, 'utf8');
101 response.end();
102}
103
Jack Franklin279564e2020-07-06 15:25:18 +0100104async function checkFileExists(filePath) {
105 try {
106 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
107 return !errorsAccessingFile;
108 } catch (e) {
109 return false;
110 }
111}
112
Jack Franklin1557a1c2020-06-08 15:22:13 +0100113async function requestHandler(request, response) {
114 const filePath = parseURL(request.url).pathname;
115
116 if (filePath === '/favicon.ico') {
117 send404(response, '404, no favicon');
118 return;
119 }
120
121 if (filePath === '/' || filePath === '/index.html') {
122 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
123 const html = createServerIndexFile(components);
124 respondWithHtml(response, html);
125 } else if (path.extname(filePath) === '') {
126 // This means it's a component path like /breadcrumbs.
127 const componentHtml = await getExamplesForPath(filePath);
128 respondWithHtml(response, componentHtml);
129 } else {
130 // This means it's an asset like a JS file.
131 const fullPath = path.join(devtoolsFrontendFolder, filePath);
132
133 if (!fullPath.startsWith(devtoolsFrontendFolder)) {
134 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
135 process.exit(1);
136 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100137
Jack Franklin279564e2020-07-06 15:25:18 +0100138 const fileExists = await checkFileExists(fullPath);
139
140 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100141 send404(response, '404, File not found');
142 return;
143 }
144
145 let encoding = 'utf8';
146 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
147 encoding = 'binary';
148 }
149
150 const fileContents = await fs.promises.readFile(fullPath, encoding);
151
152 encoding = 'utf8';
153 if (fullPath.endsWith('.js')) {
154 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
155 } else if (fullPath.endsWith('.css')) {
156 response.setHeader('Content-Type', 'text/css; charset=utf-8');
157 } else if (fullPath.endsWith('.wasm')) {
158 response.setHeader('Content-Type', 'application/wasm');
159 encoding = 'binary';
160 } else if (fullPath.endsWith('.svg')) {
161 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
162 } else if (fullPath.endsWith('.png')) {
163 response.setHeader('Content-Type', 'image/png');
164 encoding = 'binary';
165 } else if (fullPath.endsWith('.jpg')) {
166 response.setHeader('Content-Type', 'image/jpg');
167 encoding = 'binary';
168 }
169
170 response.writeHead(200);
171 response.write(fileContents, encoding);
172 response.end();
173 }
174}