blob: 0e7b77c29fd4ea2e70f70024ddee7bcd7bbddfc7 [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);
Jack Franklinc1501222020-10-02 09:42:08 +010086 const allFiles = await fs.promises.readdir(componentDirectory);
87 const htmlExampleFiles = allFiles.filter(file => {
88 return path.extname(file) === '.html';
89 });
Jack Franklin1557a1c2020-06-08 15:22:13 +010090
Jack Franklinc1501222020-10-02 09:42:08 +010091 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +010092}
93
94function respondWithHtml(response, html) {
95 response.setHeader('Content-Type', 'text/html; charset=utf-8');
96 response.writeHead(200);
97 response.write(html, 'utf8');
98 response.end();
99}
100
101function send404(response, message) {
102 response.writeHead(404);
103 response.write(message, 'utf8');
104 response.end();
105}
106
Jack Franklin279564e2020-07-06 15:25:18 +0100107async function checkFileExists(filePath) {
108 try {
109 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
110 return !errorsAccessingFile;
111 } catch (e) {
112 return false;
113 }
114}
115
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100116/**
117 * In Devtools-Frontend we load images without a leading slash, e.g.
118 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100119 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100120 * So we check if the path ends in Images/*.* and if so, remove anything before
121 * it. Then it will be resolved correctly.
122 */
123function normalizeImagePathIfRequired(filePath) {
124 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
125 const match = imagePathRegex.exec(filePath);
126 if (!match) {
127 return filePath;
128 }
129
130 const [, imageName, imageExt] = match;
131 const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
132 return normalizedPath;
133}
134
Jack Franklin1557a1c2020-06-08 15:22:13 +0100135async function requestHandler(request, response) {
136 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100137 if (filePath === '/favicon.ico') {
138 send404(response, '404, no favicon');
139 return;
140 }
141
142 if (filePath === '/' || filePath === '/index.html') {
143 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
144 const html = createServerIndexFile(components);
145 respondWithHtml(response, html);
146 } else if (path.extname(filePath) === '') {
147 // This means it's a component path like /breadcrumbs.
148 const componentHtml = await getExamplesForPath(filePath);
149 respondWithHtml(response, componentHtml);
150 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100151 // This means it's an asset like a JS file or an image.
152 const normalizedPath = normalizeImagePathIfRequired(filePath);
153 const fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100154
155 if (!fullPath.startsWith(devtoolsFrontendFolder)) {
156 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
157 process.exit(1);
158 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100159
Jack Franklin279564e2020-07-06 15:25:18 +0100160 const fileExists = await checkFileExists(fullPath);
161
162 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100163 send404(response, '404, File not found');
164 return;
165 }
166
167 let encoding = 'utf8';
168 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
169 encoding = 'binary';
170 }
171
172 const fileContents = await fs.promises.readFile(fullPath, encoding);
173
174 encoding = 'utf8';
175 if (fullPath.endsWith('.js')) {
176 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
177 } else if (fullPath.endsWith('.css')) {
178 response.setHeader('Content-Type', 'text/css; charset=utf-8');
179 } else if (fullPath.endsWith('.wasm')) {
180 response.setHeader('Content-Type', 'application/wasm');
181 encoding = 'binary';
182 } else if (fullPath.endsWith('.svg')) {
183 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
184 } else if (fullPath.endsWith('.png')) {
185 response.setHeader('Content-Type', 'image/png');
186 encoding = 'binary';
187 } else if (fullPath.endsWith('.jpg')) {
188 response.setHeader('Content-Type', 'image/jpg');
189 encoding = 'binary';
190 }
191
192 response.writeHead(200);
193 response.write(fileContents, encoding);
194 response.end();
195 }
196}