blob: ae46054491dec67ad7a4f78db5cbc36dae1f8568 [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>
Jack Franklinb5997162020-11-25 17:28:51 +000069 a:link, a:visited {
70 color: blue;
71 text-transform: capitalize;
72 text-decoration: none;
73 }
74 a:hover {
75 text-decoration: underline;
76 }
Jack Franklin1557a1c2020-06-08 15:22:13 +010077 </style>
78 </head>
79 <body>
80 <h1>DevTools components</h1>
81 <ul>
82 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +000083 const niceName = name.replace(/_/g, ' ');
84 return `<li><a href='/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +010085 }).join('\n')}
86 </ul>
87 </body>
88 </html>`;
89 // clang-format on
90}
91
92async function getExamplesForPath(filePath) {
93 const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
Jack Franklinc1501222020-10-02 09:42:08 +010094 const allFiles = await fs.promises.readdir(componentDirectory);
95 const htmlExampleFiles = allFiles.filter(file => {
96 return path.extname(file) === '.html';
97 });
Jack Franklin1557a1c2020-06-08 15:22:13 +010098
Jack Franklinc1501222020-10-02 09:42:08 +010099 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100100}
101
102function respondWithHtml(response, html) {
103 response.setHeader('Content-Type', 'text/html; charset=utf-8');
104 response.writeHead(200);
105 response.write(html, 'utf8');
106 response.end();
107}
108
109function send404(response, message) {
110 response.writeHead(404);
111 response.write(message, 'utf8');
112 response.end();
113}
114
Jack Franklin279564e2020-07-06 15:25:18 +0100115async function checkFileExists(filePath) {
116 try {
117 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
118 return !errorsAccessingFile;
119 } catch (e) {
120 return false;
121 }
122}
123
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100124/**
125 * In Devtools-Frontend we load images without a leading slash, e.g.
126 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100127 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100128 * So we check if the path ends in Images/*.* and if so, remove anything before
129 * it. Then it will be resolved correctly.
130 */
131function normalizeImagePathIfRequired(filePath) {
132 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
133 const match = imagePathRegex.exec(filePath);
134 if (!match) {
135 return filePath;
136 }
137
138 const [, imageName, imageExt] = match;
139 const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
140 return normalizedPath;
141}
142
Jack Franklin1557a1c2020-06-08 15:22:13 +0100143async function requestHandler(request, response) {
144 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100145 if (filePath === '/favicon.ico') {
146 send404(response, '404, no favicon');
147 return;
148 }
149
150 if (filePath === '/' || filePath === '/index.html') {
151 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000152 const html = createServerIndexFile(components.filter(filePath => {
153 const stats = fs.lstatSync(path.join(devtoolsFrontendFolder, 'component_docs', filePath));
154 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
155 return stats.isDirectory();
156 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100157 respondWithHtml(response, html);
158 } else if (path.extname(filePath) === '') {
159 // This means it's a component path like /breadcrumbs.
160 const componentHtml = await getExamplesForPath(filePath);
161 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000162 return;
163 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
164 /** This conditional checks if we are viewing an individual example's HTML
165 * file. e.g. localhost:8090/component_docs/data_grid/basic.html For each
166 * example we inject themeColors.css into the page so all CSS variables
167 * that components use are available.
168 */
169 const fileContents = await fs.promises.readFile(path.join(devtoolsFrontendFolder, filePath), {encoding: 'utf8'});
170 const themeColoursLink = '<link rel="stylesheet" href="/ui/themeColors.css" type="text/css" />';
171 const toggleDarkModeScript = '<script type="module" src="/component_docs/component_docs.js"></script>';
172 const newFileContents = fileContents.replace('<style>', themeColoursLink + '\n<style>')
173 .replace('<script', toggleDarkModeScript + '\n<script');
174 respondWithHtml(response, newFileContents);
175
Jack Franklin1557a1c2020-06-08 15:22:13 +0100176 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100177 // This means it's an asset like a JS file or an image.
178 const normalizedPath = normalizeImagePathIfRequired(filePath);
179 const fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100180
181 if (!fullPath.startsWith(devtoolsFrontendFolder)) {
182 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
183 process.exit(1);
184 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100185
Jack Franklin279564e2020-07-06 15:25:18 +0100186 const fileExists = await checkFileExists(fullPath);
187
188 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100189 send404(response, '404, File not found');
190 return;
191 }
192
193 let encoding = 'utf8';
194 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
195 encoding = 'binary';
196 }
197
198 const fileContents = await fs.promises.readFile(fullPath, encoding);
199
200 encoding = 'utf8';
201 if (fullPath.endsWith('.js')) {
202 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
203 } else if (fullPath.endsWith('.css')) {
204 response.setHeader('Content-Type', 'text/css; charset=utf-8');
205 } else if (fullPath.endsWith('.wasm')) {
206 response.setHeader('Content-Type', 'application/wasm');
207 encoding = 'binary';
208 } else if (fullPath.endsWith('.svg')) {
209 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
210 } else if (fullPath.endsWith('.png')) {
211 response.setHeader('Content-Type', 'image/png');
212 encoding = 'binary';
213 } else if (fullPath.endsWith('.jpg')) {
214 response.setHeader('Content-Type', 'image/jpg');
215 encoding = 'binary';
216 }
217
218 response.writeHead(200);
219 response.write(fileContents, encoding);
220 response.end();
221 }
222}