Provide --component-docs-base argument to components server

The component server assumes that the examples live in
root/out/Target/front_end/component_docs, but now you can pass
--component-docs-base to change that.

Bug: 1182255
Change-Id: I143aee02bcd2a32e4f6ea163ac5edd5f44156bdc
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2727422
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
diff --git a/scripts/component_server/server.js b/scripts/component_server/server.js
index 4564232..e0374db 100644
--- a/scripts/component_server/server.js
+++ b/scripts/component_server/server.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Copyright 2021 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -20,6 +20,14 @@
 const sharedResourcesBase = argv.sharedResourcesBase || '/';
 
 /**
+ * The server assumes that examples live in
+ * devtoolsRoot/out/Target/front_end/component_docs, but if you need to add a
+ * prefix you can pass this argument. Passing `foo` will redirect the server to
+ * look in devtoolsRoot/out/Target/foo/front_end/component_docs.
+ */
+const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE || '';
+
+/**
  * When you run npm run components-server we run the script as is from scripts/,
  * but when this server is run as part of a test suite it's run from
  * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
@@ -43,6 +51,7 @@
     isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
 
 const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
+const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg);
 
 if (!fs.existsSync(devtoolsRootFolder)) {
   console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
@@ -59,6 +68,8 @@
     process.send(serverPort);
   }
   console.log(`Started components server at http://localhost:${serverPort}\n`);
+  console.log(`component_docs location: ${
+      path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'component_docs'))}`);
 });
 
 server.once('error', error => {
@@ -157,7 +168,7 @@
 }
 
 async function getExamplesForPath(filePath) {
-  const componentDirectory = path.join(devtoolsRootFolder, filePath);
+  const componentDirectory = path.join(componentDocsBaseFolder, filePath);
   const allFiles = await fs.promises.readdir(componentDirectory);
   const htmlExampleFiles = allFiles.filter(file => {
     return path.extname(file) === '.html';
@@ -215,9 +226,9 @@
   }
 
   if (filePath === '/' || filePath === '/index.html') {
-    const components = await fs.promises.readdir(path.join(devtoolsRootFolder, 'front_end', 'component_docs'));
+    const components = await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'component_docs'));
     const html = createServerIndexFile(components.filter(filePath => {
-      const stats = fs.lstatSync(path.join(devtoolsRootFolder, 'front_end', 'component_docs', filePath));
+      const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'component_docs', filePath));
       // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
       return stats.isDirectory();
     }));
@@ -234,17 +245,25 @@
      *  that components use are available.
      */
 
-    // server --base=.
-
-    // server --base=devtools-frontend
-
-    const fileContents = await fs.promises.readFile(path.join(devtoolsRootFolder, filePath), {encoding: 'utf8'});
+    /**
+    * We also let the user provide a different base path for any shared
+    * resources that we load. But if this is provided along with the
+    * componentDocsBaseArg, and the two are the same, we don't want to use the
+    * shared resources base, as it's part of the componentDocsBaseArg and
+    * therefore the URL is already correct.
+    *
+    * If we didn't get a componentDocsBaseArg or we did and it's different to
+    * the sharedResourcesBase, we use sharedResourcesBase.
+    */
+    const baseUrlForSharedResource =
+        componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`;
+    const fileContents = await fs.promises.readFile(path.join(componentDocsBaseFolder, filePath), {encoding: 'utf8'});
     const themeColoursLink = `<link rel="stylesheet" href="${
-        path.join(sharedResourcesBase, 'front_end', 'ui', 'themeColors.css')}" type="text/css" />`;
+        path.join(baseUrlForSharedResource, 'front_end', 'ui', 'themeColors.css')}" type="text/css" />`;
     const inspectorCommonLink = `<link rel="stylesheet" href="${
-        path.join(sharedResourcesBase, 'front_end', 'ui', 'inspectorCommon.css')}" type="text/css" />`;
+        path.join(baseUrlForSharedResource, 'front_end', 'ui', 'inspectorCommon.css')}" type="text/css" />`;
     const toggleDarkModeScript = `<script type="module" src="${
-        path.join(sharedResourcesBase, 'front_end', 'component_docs', 'component_docs.js')}"></script>`;
+        path.join(baseUrlForSharedResource, 'front_end', 'component_docs', 'component_docs.js')}"></script>`;
     const newFileContents = fileContents.replace('<style>', `${themeColoursLink}\n${inspectorCommonLink}\n<style>`)
                                 .replace('<script', toggleDarkModeScript + '\n<script');
     respondWithHtml(response, newFileContents);
@@ -253,10 +272,10 @@
     // This means it's an asset like a JS file or an image.
     const normalizedPath = normalizeImagePathIfRequired(filePath);
 
-    let fullPath = path.join(devtoolsRootFolder, normalizedPath);
+    let fullPath = path.join(componentDocsBaseFolder, normalizedPath);
     if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
       // Rewrite this path so we can load up the locale in the component-docs
-      fullPath = path.join(devtoolsRootFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
+      fullPath = path.join(componentDocsBaseFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
     }
 
     if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {