blob: 63e1f7a364c2978f149581134ed7ccb5cc18a5bf [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 Franklin9d4ecf72020-09-16 14:04:30 +0100113/**
114 * In Devtools-Frontend we load images without a leading slash, e.g.
115 * url(Images/checker.png). This works within devtools, but breaks this component
116 * server as the path ends up as /component_docs/my_component/Image/checker.png.
117 * So we check if the path ends in Images/*.* and if so, remove anything before
118 * it. Then it will be resolved correctly.
119 */
120function normalizeImagePathIfRequired(filePath) {
121 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
122 const match = imagePathRegex.exec(filePath);
123 if (!match) {
124 return filePath;
125 }
126
127 const [, imageName, imageExt] = match;
128 const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
129 return normalizedPath;
130}
131
Jack Franklin1557a1c2020-06-08 15:22:13 +0100132async function requestHandler(request, response) {
133 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100134 if (filePath === '/favicon.ico') {
135 send404(response, '404, no favicon');
136 return;
137 }
138
139 if (filePath === '/' || filePath === '/index.html') {
140 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
141 const html = createServerIndexFile(components);
142 respondWithHtml(response, html);
143 } else if (path.extname(filePath) === '') {
144 // This means it's a component path like /breadcrumbs.
145 const componentHtml = await getExamplesForPath(filePath);
146 respondWithHtml(response, componentHtml);
147 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100148 // This means it's an asset like a JS file or an image.
149 const normalizedPath = normalizeImagePathIfRequired(filePath);
150 const fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100151
152 if (!fullPath.startsWith(devtoolsFrontendFolder)) {
153 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
154 process.exit(1);
155 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100156
Jack Franklin279564e2020-07-06 15:25:18 +0100157 const fileExists = await checkFileExists(fullPath);
158
159 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100160 send404(response, '404, File not found');
161 return;
162 }
163
164 let encoding = 'utf8';
165 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
166 encoding = 'binary';
167 }
168
169 const fileContents = await fs.promises.readFile(fullPath, encoding);
170
171 encoding = 'utf8';
172 if (fullPath.endsWith('.js')) {
173 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
174 } else if (fullPath.endsWith('.css')) {
175 response.setHeader('Content-Type', 'text/css; charset=utf-8');
176 } else if (fullPath.endsWith('.wasm')) {
177 response.setHeader('Content-Type', 'application/wasm');
178 encoding = 'binary';
179 } else if (fullPath.endsWith('.svg')) {
180 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
181 } else if (fullPath.endsWith('.png')) {
182 response.setHeader('Content-Type', 'image/png');
183 encoding = 'binary';
184 } else if (fullPath.endsWith('.jpg')) {
185 response.setHeader('Content-Type', 'image/jpg');
186 encoding = 'binary';
187 }
188
189 response.writeHead(200);
190 response.write(fileContents, encoding);
191 response.end();
192 }
193}