From fea5cec5eb9b26cbf42f98f6ef79a8e01028af0e Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 5 Oct 2021 17:00:10 +0200 Subject: [PATCH] [NO-JIRA] Fix frontend caching issue --- server/sonar-web/config/esbuild-config.js | 54 ++++++++++------- .../sonar-web/config/esbuild-html-plugin.js | 60 +++++++++++++++++++ .../indexHtmlTemplate.js} | 25 +++++++- server/sonar-web/scripts/build.js | 2 +- server/sonar-web/scripts/start.js | 39 +++++++++++- 5 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 server/sonar-web/config/esbuild-html-plugin.js rename server/sonar-web/{public/index.html => config/indexHtmlTemplate.js} (64%) diff --git a/server/sonar-web/config/esbuild-config.js b/server/sonar-web/config/esbuild-config.js index c1cec016ba5..8497d8978da 100644 --- a/server/sonar-web/config/esbuild-config.js +++ b/server/sonar-web/config/esbuild-config.js @@ -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 index 00000000000..7ab0a9ff5f0 --- /dev/null +++ b/server/sonar-web/config/esbuild-html-plugin.js @@ -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/public/index.html b/server/sonar-web/config/indexHtmlTemplate.js similarity index 64% rename from server/sonar-web/public/index.html rename to server/sonar-web/config/indexHtmlTemplate.js index 7c6fb6a4194..0a0695cfc79 100644 --- a/server/sonar-web/public/index.html +++ b/server/sonar-web/config/indexHtmlTemplate.js @@ -1,3 +1,23 @@ +/* + * 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) => ` @@ -20,7 +40,7 @@ %INSTANCE% - + @@ -38,7 +58,8 @@ window.official = %OFFICIAL%; - + +`; diff --git a/server/sonar-web/scripts/build.js b/server/sonar-web/scripts/build.js index 42d461449f9..d81c59adb0f 100644 --- a/server/sonar-web/scripts/build.js +++ b/server/sonar-web/scripts/build.js @@ -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')); diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js index ceb9cc1cdb4..64fc06ca851 100644 --- a/server/sonar-web/scripts/start.js +++ b/server/sonar-web/scripts/start.js @@ -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); -- 2.39.5