]> source.dussan.org Git - sonarqube.git/commitdiff
[NO-JIRA] Fix frontend caching issue
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 5 Oct 2021 15:00:10 +0000 (17:00 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 6 Oct 2021 20:03:08 +0000 (20:03 +0000)
server/sonar-web/config/esbuild-config.js
server/sonar-web/config/esbuild-html-plugin.js [new file with mode: 0644]
server/sonar-web/config/indexHtmlTemplate.js [new file with mode: 0644]
server/sonar-web/public/index.html [deleted file]
server/sonar-web/scripts/build.js
server/sonar-web/scripts/start.js

index c1cec016ba5c0754c75ad35207249a537de5a096..8497d8978dac3e808e22aafa7c85a962518bdfe2 100644 (file)
@@ -22,26 +22,12 @@ const postCssPlugin = require('esbuild-plugin-postcss2').default;
 const postCssCalc = require('postcss-calc');
 const postCssCustomProperties = require('postcss-custom-properties');
 const documentationPlugin = require('./esbuild-documentation-plugin');
+const htmlPlugin = require('./esbuild-html-plugin');
+const htmlTemplate = require('./indexHtmlTemplate');
 const { getCustomProperties } = require('./utils');
 
-module.exports = release => ({
-  entryPoints: ['src/main/js/app/index.ts'],
-  tsconfig: './tsconfig.json',
-  external: ['/images/*'],
-  loader: {
-    '.png': 'dataurl',
-    '.md': 'text'
-  },
-  define: {
-    'process.cwd': 'dummy_process_cwd'
-  },
-  inject: ['config/process-shim.js'],
-  bundle: true,
-  minify: release,
-  sourcemap: true,
-  target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
-  outfile: 'build/webapp/js/out.js',
-  plugins: [
+module.exports = release => {
+  const plugins = [
     postCssPlugin({
       plugins: [
         autoprefixer,
@@ -53,5 +39,33 @@ module.exports = release => ({
       ]
     }),
     documentationPlugin()
-  ]
-});
+  ];
+
+  if (release) {
+    // Only create index.html from template when releasing
+    // The devserver will generate its own index file from the template
+    plugins.push(htmlPlugin());
+  }
+
+  return {
+    entryPoints: ['src/main/js/app/index.ts'],
+    tsconfig: './tsconfig.json',
+    external: ['/images/*'],
+    loader: {
+      '.png': 'dataurl',
+      '.md': 'text'
+    },
+    define: {
+      'process.cwd': 'dummy_process_cwd'
+    },
+    inject: ['config/process-shim.js'],
+    bundle: true,
+    minify: release,
+    metafile: true,
+    sourcemap: true,
+    target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
+    outdir: 'build/webapp/js',
+    entryNames: release ? 'out[hash]' : 'out',
+    plugins
+  };
+};
diff --git a/server/sonar-web/config/esbuild-html-plugin.js b/server/sonar-web/config/esbuild-html-plugin.js
new file mode 100644 (file)
index 0000000..7ab0a9f
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+const fs = require('fs-extra');
+const path = require('path');
+const paths = require('./paths');
+const htmlTemplate = require('./indexHtmlTemplate');
+
+function extractHash(filename) {
+  const regexp = /out([\w]+)\./;
+  const result = filename.match(regexp);
+  if (!result) {
+    throw Error('filename format error: could not extract hash');
+  }
+
+  return result[1];
+}
+
+/*
+ * This plugin generates a index.html file from the template,
+ * injecting the right hash values to the imported js and css files
+ */
+module.exports = () => ({
+  name: 'html-plugin',
+  setup({ onEnd }) {
+    onEnd(result => {
+      const files = result.metafile.outputs;
+
+      let cssHash;
+      let jsHash;
+      for (const filename in files) {
+        if (filename.endsWith('css')) {
+          cssHash = extractHash(filename);
+        } else if (filename.endsWith('js')) {
+          jsHash = extractHash(filename);
+        }
+      }
+
+      const htmlContents = htmlTemplate(cssHash, jsHash);
+
+      fs.writeFile(path.join(paths.appBuild, 'index.html'), htmlContents);
+    });
+  }
+});
diff --git a/server/sonar-web/config/indexHtmlTemplate.js b/server/sonar-web/config/indexHtmlTemplate.js
new file mode 100644 (file)
index 0000000..0a0695c
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+module.exports = (cssHash, jsHash) => `
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <link rel="apple-touch-icon" href="%WEB_CONTEXT%/apple-touch-icon.png">
+    <link rel="apple-touch-icon" sizes="57x57" href="%WEB_CONTEXT%/apple-touch-icon-57x57.png">
+    <link rel="apple-touch-icon" sizes="60x60" href="%WEB_CONTEXT%/apple-touch-icon-60x60.png">
+    <link rel="apple-touch-icon" sizes="72x72" href="%WEB_CONTEXT%/apple-touch-icon-72x72.png">
+    <link rel="apple-touch-icon" sizes="76x76" href="%WEB_CONTEXT%/apple-touch-icon-76x76.png">
+    <link rel="apple-touch-icon" sizes="114x114" href="%WEB_CONTEXT%/apple-touch-icon-114x114.png">
+    <link rel="apple-touch-icon" sizes="120x120" href="%WEB_CONTEXT%/apple-touch-icon-120x120.png">
+    <link rel="apple-touch-icon" sizes="144x144" href="%WEB_CONTEXT%/apple-touch-icon-144x144.png">
+    <link rel="apple-touch-icon" sizes="152x152" href="%WEB_CONTEXT%/apple-touch-icon-152x152.png">
+    <link rel="apple-touch-icon" sizes="180x180" href="%WEB_CONTEXT%/apple-touch-icon-180x180.png">
+    <link rel="icon" type="image/x-icon" href="%WEB_CONTEXT%/favicon.ico">
+    <meta name="application-name" content="SonarQube" />
+    <meta name="msapplication-TileColor" content="#FFFFFF" />
+    <meta name="msapplication-TileImage" content="%WEB_CONTEXT%/mstile-512x512.png" />
+    <title>%INSTANCE%</title>
+
+    <link rel="stylesheet" href="%WEB_CONTEXT%/js/out${cssHash}.css">
+</head>
+
+<body>
+    <div id="content">
+        <div class="global-loading">
+            <i class="spinner global-loading-spinner"></i> 
+            <span class="global-loading-text">Loading...</span>
+        </div>
+    </div>
+
+    <script>
+        window.baseUrl = '%WEB_CONTEXT%';
+        window.serverStatus = '%SERVER_STATUS%';
+        window.instance = '%INSTANCE%';
+        window.official = %OFFICIAL%;
+    </script>
+
+    <script type="module" src="%WEB_CONTEXT%/js/out${jsHash}.js"></script>
+</body>
+
+</html>
+`;
diff --git a/server/sonar-web/public/index.html b/server/sonar-web/public/index.html
deleted file mode 100644 (file)
index 7c6fb6a..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <link rel="apple-touch-icon" href="%WEB_CONTEXT%/apple-touch-icon.png">
-    <link rel="apple-touch-icon" sizes="57x57" href="%WEB_CONTEXT%/apple-touch-icon-57x57.png">
-    <link rel="apple-touch-icon" sizes="60x60" href="%WEB_CONTEXT%/apple-touch-icon-60x60.png">
-    <link rel="apple-touch-icon" sizes="72x72" href="%WEB_CONTEXT%/apple-touch-icon-72x72.png">
-    <link rel="apple-touch-icon" sizes="76x76" href="%WEB_CONTEXT%/apple-touch-icon-76x76.png">
-    <link rel="apple-touch-icon" sizes="114x114" href="%WEB_CONTEXT%/apple-touch-icon-114x114.png">
-    <link rel="apple-touch-icon" sizes="120x120" href="%WEB_CONTEXT%/apple-touch-icon-120x120.png">
-    <link rel="apple-touch-icon" sizes="144x144" href="%WEB_CONTEXT%/apple-touch-icon-144x144.png">
-    <link rel="apple-touch-icon" sizes="152x152" href="%WEB_CONTEXT%/apple-touch-icon-152x152.png">
-    <link rel="apple-touch-icon" sizes="180x180" href="%WEB_CONTEXT%/apple-touch-icon-180x180.png">
-    <link rel="icon" type="image/x-icon" href="%WEB_CONTEXT%/favicon.ico">
-    <meta name="application-name" content="SonarQube" />
-    <meta name="msapplication-TileColor" content="#FFFFFF" />
-    <meta name="msapplication-TileImage" content="%WEB_CONTEXT%/mstile-512x512.png" />
-    <title>%INSTANCE%</title>
-
-    <link rel="stylesheet" href="%WEB_CONTEXT%/js/out.css">
-</head>
-
-<body>
-    <div id="content">
-        <div class="global-loading">
-            <i class="spinner global-loading-spinner"></i> 
-            <span class="global-loading-text">Loading...</span>
-        </div>
-    </div>
-
-    <script>
-        window.baseUrl = '%WEB_CONTEXT%';
-        window.serverStatus = '%SERVER_STATUS%';
-        window.instance = '%INSTANCE%';
-        window.official = %OFFICIAL%;
-    </script>
-
-    <script type="module" src="%WEB_CONTEXT%/js/out.js"></script>
-</body>
-
-</html>
index 42d461449f9ced6e803742f48b625622e6a202a3..d81c59adb0f8a3a0eb10aafa77c07e5d9d690317 100644 (file)
@@ -39,7 +39,7 @@ async function build() {
   console.log(chalk.cyan.bold(`Creating ${release ? 'optimized' : 'fast'} production build...`));
   console.log();
 
-  await esbuild.build(getConfig(release));
+  await esbuild.build(getConfig(release)).catch(() => process.exit(1));
 
   console.log(chalk.green.bold('Compiled successfully!'));
   console.log(chalk.cyan(Math.round(performance.now() - start), 'ms'));
index ceb9cc1cdb47cb2a081699e6600a98f85e75f0f6..64fc06ca851caade8fb5484c9eefed382cb2bffe 100644 (file)
@@ -27,6 +27,10 @@ const http = require('http');
 const httpProxy = require('http-proxy');
 const getConfig = require('../config/esbuild-config');
 const { getMessages } = require('./utils');
+const paths = require('../config/paths');
+
+const STATUS_OK = 200;
+const STATUS_ERROR = 500;
 
 const port = process.env.PORT || 3000;
 const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
@@ -38,16 +42,39 @@ const config = getConfig(false);
 function handleL10n(res) {
   getMessages()
     .then(messages => {
-      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.writeHead(STATUS_OK, { 'Content-Type': 'application/json' });
       res.end(JSON.stringify({ effectiveLocale: 'en', messages }));
     })
     .catch(e => {
       console.error(e);
-      res.writeHead(500);
+      res.writeHead(STATUS_ERROR);
       res.end(e);
     });
 }
 
+function handleStaticFileRequest(req, res) {
+  fs.readFile(paths.appBuild + req.url, (err, data) => {
+    if (err) {
+      // Any unknown path should go to the index.html
+      const htmlTemplate = require('../config/indexHtmlTemplate');
+
+      // Replace hash placeholders as well as all the
+      // tags that are usually replaced by the server
+      const content = htmlTemplate('', '')
+        .replace(/%WEB_CONTEXT%/g, '')
+        .replace(/%SERVER_STATUS%/g, 'UP')
+        .replace(/%INSTANCE%/g, 'SonarQube')
+        .replace(/%OFFICIAL%/g, 'true');
+
+      res.writeHead(STATUS_OK);
+      res.end(content);
+    } else {
+      res.writeHead(STATUS_OK);
+      res.end(data);
+    }
+  });
+}
+
 function run() {
   console.log('starting...');
   esbuild
@@ -83,7 +110,11 @@ function run() {
             esbuildProxy.web(req, res);
           } else if (req.url.match(/l10n\/index/)) {
             handleL10n(res);
-          } else {
+          } else if (
+            req.url.includes('api/') ||
+            req.url.includes('images/') ||
+            req.url.includes('static/')
+          ) {
             proxy.web(
               req,
               res,
@@ -92,6 +123,8 @@ function run() {
               },
               e => console.error('req error', e)
             );
+          } else {
+            handleStaticFileRequest(req, res);
           }
         })
         .listen(port);