blob: 092ab98cf3b73cc01bd2f2a4721f2ad1f2fd1462 [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';
Jack Franklin086ccd52020-11-27 11:00:14 +000014
15/**
16 * When you run npm run components-server we run the script as is from scripts/,
17 * but when this server is run as part of a test suite it's run from
18 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
19 * out where we are.
20 */
21const isRunningInGen = __dirname.includes(path.join(path.sep, 'gen', path.sep, 'scripts'));
22const pathToOutDirectory = isRunningInGen ? path.resolve(path.join(__dirname), '..', '..', '..', '..') :
23 path.resolve(path.join(__dirname, '..', '..', 'out'));
24const devtoolsFrontendFolder = path.resolve(path.join(pathToOutDirectory, target, 'gen', 'front_end'));
Jack Franklin1557a1c2020-06-08 15:22:13 +010025
26if (!fs.existsSync(devtoolsFrontendFolder)) {
27 console.error(`ERROR: Generated front_end folder (${devtoolsFrontendFolder}) does not exist.`);
28 console.log(
29 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
30 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
31 process.exit(1);
32}
33
Jack Franklin086ccd52020-11-27 11:00:14 +000034const server = http.createServer(requestHandler);
35server.listen(serverPort);
36server.once('listening', () => {
37 if (process.send) {
38 process.send(serverPort);
39 }
40 console.log(`Started components server at http://localhost:${serverPort}\n`);
41});
42
43server.once('error', error => {
44 if (process.send) {
45 process.send('ERROR');
46 }
47 throw error;
48});
Jack Franklin1557a1c2020-06-08 15:22:13 +010049
50function createComponentIndexFile(componentPath, componentExamples) {
Changhao Hand2454322020-08-11 15:09:35 +020051 const componentName = componentPath.replace('/', '').replace(/_/g, ' ');
Jack Franklin1557a1c2020-06-08 15:22:13 +010052 // clang-format off
53 return `<!DOCTYPE html>
54 <html>
55 <head>
56 <meta charset="UTF-8" />
57 <meta name="viewport" content="width=device-width" />
58 <title>DevTools component: ${componentName}</title>
59 <style>
60 h1 { text-transform: capitalize; }
61
62 .example {
63 padding: 5px;
64 margin: 10px;
65 }
66 iframe { display: block; width: 100%; }
67 </style>
68 </head>
69 <body>
70 <h1>${componentName}</h1>
71 ${componentExamples.map(example => {
Jack Franklin279564e2020-07-06 15:25:18 +010072 const fullPath = path.join('component_docs', componentPath, example);
Jack Franklin1557a1c2020-06-08 15:22:13 +010073 return `<div class="example">
74 <h3><a href="${fullPath}">${example}</a></h3>
75 <iframe src="${fullPath}"></iframe>
76 </div>`;
77 }).join('\n')}
78 </body>
79 </html>`;
80 // clang-format on
81}
82
83function createServerIndexFile(componentNames) {
84 // clang-format off
85 return `<!DOCTYPE html>
86 <html>
87 <head>
88 <meta charset="UTF-8" />
89 <meta name="viewport" content="width=device-width" />
90 <title>DevTools components</title>
91 <style>
Jack Franklinb5997162020-11-25 17:28:51 +000092 a:link, a:visited {
93 color: blue;
94 text-transform: capitalize;
95 text-decoration: none;
96 }
97 a:hover {
98 text-decoration: underline;
99 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100100 </style>
101 </head>
102 <body>
103 <h1>DevTools components</h1>
104 <ul>
105 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000106 const niceName = name.replace(/_/g, ' ');
107 return `<li><a href='/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100108 }).join('\n')}
109 </ul>
110 </body>
111 </html>`;
112 // clang-format on
113}
114
115async function getExamplesForPath(filePath) {
116 const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100117 const allFiles = await fs.promises.readdir(componentDirectory);
118 const htmlExampleFiles = allFiles.filter(file => {
119 return path.extname(file) === '.html';
120 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100121
Jack Franklinc1501222020-10-02 09:42:08 +0100122 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100123}
124
125function respondWithHtml(response, html) {
126 response.setHeader('Content-Type', 'text/html; charset=utf-8');
127 response.writeHead(200);
128 response.write(html, 'utf8');
129 response.end();
130}
131
132function send404(response, message) {
133 response.writeHead(404);
134 response.write(message, 'utf8');
135 response.end();
136}
137
Jack Franklin279564e2020-07-06 15:25:18 +0100138async function checkFileExists(filePath) {
139 try {
140 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
141 return !errorsAccessingFile;
142 } catch (e) {
143 return false;
144 }
145}
146
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100147/**
148 * In Devtools-Frontend we load images without a leading slash, e.g.
149 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100150 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100151 * So we check if the path ends in Images/*.* and if so, remove anything before
152 * it. Then it will be resolved correctly.
153 */
154function normalizeImagePathIfRequired(filePath) {
155 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
156 const match = imagePathRegex.exec(filePath);
157 if (!match) {
158 return filePath;
159 }
160
161 const [, imageName, imageExt] = match;
162 const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
163 return normalizedPath;
164}
165
Jack Franklin1557a1c2020-06-08 15:22:13 +0100166async function requestHandler(request, response) {
167 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100168 if (filePath === '/favicon.ico') {
169 send404(response, '404, no favicon');
170 return;
171 }
172
173 if (filePath === '/' || filePath === '/index.html') {
174 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000175 const html = createServerIndexFile(components.filter(filePath => {
176 const stats = fs.lstatSync(path.join(devtoolsFrontendFolder, 'component_docs', filePath));
177 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
178 return stats.isDirectory();
179 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100180 respondWithHtml(response, html);
181 } else if (path.extname(filePath) === '') {
182 // This means it's a component path like /breadcrumbs.
183 const componentHtml = await getExamplesForPath(filePath);
184 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000185 return;
186 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
187 /** This conditional checks if we are viewing an individual example's HTML
188 * file. e.g. localhost:8090/component_docs/data_grid/basic.html For each
189 * example we inject themeColors.css into the page so all CSS variables
190 * that components use are available.
191 */
192 const fileContents = await fs.promises.readFile(path.join(devtoolsFrontendFolder, filePath), {encoding: 'utf8'});
193 const themeColoursLink = '<link rel="stylesheet" href="/ui/themeColors.css" type="text/css" />';
194 const toggleDarkModeScript = '<script type="module" src="/component_docs/component_docs.js"></script>';
195 const newFileContents = fileContents.replace('<style>', themeColoursLink + '\n<style>')
196 .replace('<script', toggleDarkModeScript + '\n<script');
197 respondWithHtml(response, newFileContents);
198
Jack Franklin1557a1c2020-06-08 15:22:13 +0100199 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100200 // This means it's an asset like a JS file or an image.
201 const normalizedPath = normalizeImagePathIfRequired(filePath);
202 const fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100203
204 if (!fullPath.startsWith(devtoolsFrontendFolder)) {
205 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
206 process.exit(1);
207 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100208
Jack Franklin279564e2020-07-06 15:25:18 +0100209 const fileExists = await checkFileExists(fullPath);
210
211 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100212 send404(response, '404, File not found');
213 return;
214 }
215
216 let encoding = 'utf8';
217 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
218 encoding = 'binary';
219 }
220
221 const fileContents = await fs.promises.readFile(fullPath, encoding);
222
223 encoding = 'utf8';
224 if (fullPath.endsWith('.js')) {
225 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
226 } else if (fullPath.endsWith('.css')) {
227 response.setHeader('Content-Type', 'text/css; charset=utf-8');
228 } else if (fullPath.endsWith('.wasm')) {
229 response.setHeader('Content-Type', 'application/wasm');
230 encoding = 'binary';
231 } else if (fullPath.endsWith('.svg')) {
232 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
233 } else if (fullPath.endsWith('.png')) {
234 response.setHeader('Content-Type', 'image/png');
235 encoding = 'binary';
236 } else if (fullPath.endsWith('.jpg')) {
237 response.setHeader('Content-Type', 'image/jpg');
238 encoding = 'binary';
239 }
240
241 response.writeHead(200);
242 response.write(fileContents, encoding);
243 response.end();
244 }
245}