diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 96225241b7..33b57f699e 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -6,9 +6,7 @@ const Url = require('url') const Path = require('path') const moment = require('moment') const pug = require('pug-runtime') - -const IS_DEV_ENV = ['development', 'test'].includes(process.env.NODE_ENV) - +const request = require('request') const Features = require('./Features') const SessionManager = require('../Features/Authentication/SessionManager') const PackageVersions = require('./PackageVersions') @@ -16,17 +14,63 @@ const Modules = require('./Modules') const SafeHTMLSubstitute = require('../Features/Helpers/SafeHTMLSubstitution') let webpackManifest -if (!IS_DEV_ENV) { - // Only load webpack manifest file in production. In dev, the web and webpack - // containers can't coordinate, so there no guarantee that the manifest file - // exists when the web server boots. We therefore ignore the manifest file in - // dev reload - webpackManifest = require(`../../../public/manifest.json`) +switch (process.env.NODE_ENV) { + case 'production': + // Only load webpack manifest file in production. + webpackManifest = require(`../../../public/manifest.json`) + break + case 'development': + // In dev, fetch the manifest from the webpack container. + loadManifestFromWebpackDevServer() + setInterval(loadManifestFromWebpackDevServer, 10 * 1000) + break + default: + // In ci, all entries are undefined. + webpackManifest = {} +} +function loadManifestFromWebpackDevServer(done = function () {}) { + request( + { + uri: `${Settings.apis.webpack.url}/manifest.json`, + headers: { Host: 'localhost' }, + json: true, + }, + (err, res, body) => { + if (!err && res.statusCode !== 200) { + err = new Error(`webpack responded with statusCode: ${res.statusCode}`) + } + if (err) { + logger.err({ err }, 'cannot fetch webpack manifest') + return done(err) + } + webpackManifest = body + done() + } + ) +} +const IN_CI = process.env.NODE_ENV === 'test' +function getWebpackAssets(entrypoint, section) { + if (IN_CI) { + // Emit an empty list of entries in CI. + return [] + } + return webpackManifest.entrypoints[entrypoint].assets[section] || [] } const I18N_HTML_INJECTIONS = new Set() module.exports = function (webRouter, privateApiRouter, publicApiRouter) { + if (process.env.NODE_ENV === 'development') { + // In the dev-env, delay requests until we fetched the manifest once. + webRouter.use(function (req, res, next) { + if (!webpackManifest) { + loadManifestFromWebpackDevServer(next) + } else { + next() + } + }) + } + webRouter.use(function (req, res, next) { res.locals.session = req.session next() @@ -81,43 +125,25 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { res.locals.buildBaseAssetPath = function () { // Return the base asset path (including the CDN url) so that webpack can // use this to dynamically fetch scripts (e.g. PDFjs worker) - return Url.resolve(staticFilesBase, '/') + return staticFilesBase + '/' } res.locals.buildJsPath = function (jsFile) { - let path - if (IS_DEV_ENV) { - // In dev: resolve path within JS asset directory - // We are *not* guaranteed to have a manifest file when the server - // starts up - path = Path.join('/js', jsFile) - } else { - // In production: resolve path from webpack manifest file - // We are guaranteed to have a manifest file since webpack compiles in - // the build - path = `/${webpackManifest[jsFile]}` - } - - return Url.resolve(staticFilesBase, path) + return staticFilesBase + webpackManifest[jsFile] } - // Temporary hack while jQuery/Angular dependencies are *not* bundled, - // instead copied into output directory res.locals.buildCopiedJsAssetPath = function (jsFile) { - let path - if (IS_DEV_ENV) { - // In dev: resolve path to root directory - // We are *not* guaranteed to have a manifest file when the server - // starts up - path = Path.join('/', jsFile) - } else { - // In production: resolve path from webpack manifest file - // We are guaranteed to have a manifest file since webpack compiles in - // the build - path = `/${webpackManifest[jsFile]}` - } + return staticFilesBase + (webpackManifest[jsFile] || '/' + jsFile) + } - return Url.resolve(staticFilesBase, path) + res.locals.entrypointScripts = function (entrypoint) { + const chunks = getWebpackAssets(entrypoint, 'js') + return chunks.map(chunk => staticFilesBase + chunk) + } + + res.locals.entrypointStyles = function (entrypoint) { + const chunks = getWebpackAssets(entrypoint, 'css') + return chunks.map(chunk => staticFilesBase + chunk) } res.locals.mathJaxPath = `/js/libs/mathjax/MathJax.js?${querystring.stringify( @@ -150,20 +176,7 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { } res.locals.buildStylesheetPath = function (cssFileName) { - let path - if (IS_DEV_ENV) { - // In dev: resolve path within CSS asset directory - // We are *not* guaranteed to have a manifest file when the server - // starts up - path = Path.join('/stylesheets/', cssFileName) - } else { - // In production: resolve path from webpack manifest file - // We are guaranteed to have a manifest file since webpack compiles in - // the build - path = `/${webpackManifest[cssFileName]}` - } - - return Url.resolve(staticFilesBase, path) + return staticFilesBase + webpackManifest[cssFileName] } res.locals.buildCssPath = function (themeModifier = '') { @@ -172,7 +185,7 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { res.locals.buildImgPath = function (imgFile) { const path = Path.join('/img/', imgFile) - return Url.resolve(staticFilesBase, path) + return staticFilesBase + path } next() diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index efb4b9544f..2f5d1fb152 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -15,7 +15,9 @@ html( //- Stylesheet link(rel='stylesheet', href=buildCssPath(getCssThemeModifier(userSettings, brandVariation)), id="main-stylesheet") - link(rel='stylesheet', href=buildStylesheetPath("libraries.css")) + block css + each file in entrypointStyles('main') + link(rel='stylesheet', href=file) block _headLinks @@ -122,8 +124,8 @@ html( != moduleIncludes("contactModal", locals) block foot-scripts - script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js')) - script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('main.js')) + each file in entrypointScripts("main") + script(type="text/javascript", nonce=scriptNonce, src=file) script(type="text/javascript", nonce=scriptNonce). //- Look for bundle var cdnBlocked = typeof Frontend === 'undefined' diff --git a/services/web/app/views/layout/layout-no-js.pug b/services/web/app/views/layout/layout-no-js.pug index c86721a810..4ddc624502 100644 --- a/services/web/app/views/layout/layout-no-js.pug +++ b/services/web/app/views/layout/layout-no-js.pug @@ -12,7 +12,8 @@ html(lang="en") link(rel="icon", href="/favicon.ico") - if buildCssPath - link(rel="stylesheet", href=buildCssPath()) + if entrypointStyles + each file in entrypointStyles('main') + link(rel='stylesheet', href=file) block body diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 44d19e2bea..0f1920e46a 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -6,8 +6,9 @@ block vars - var suppressSkipToContent = true - metadata.robotsNoindexNofollow = true -block _headLinks - link(rel='stylesheet', href=buildStylesheetPath("ide.css")) +block css + each file in entrypointStyles('ide') + link(rel='stylesheet', href=file) block content .editor(ng-controller="IdeController").full-size @@ -200,5 +201,5 @@ block append meta block foot-scripts script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js') script(type="text/javascript", nonce=scriptNonce, src=mathJaxPath) - script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js')) - script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('ide.js')) + each file in entrypointScripts("ide") + script(type="text/javascript", nonce=scriptNonce, src=file) diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index cc3f3438b8..ce5efe71ea 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -212,6 +212,9 @@ module.exports = { notifications: { url: `http://${process.env.NOTIFICATIONS_HOST || 'localhost'}:3042`, }, + webpack: { + url: `http://${process.env.WEBPACK_HOST || 'localhost'}:3808`, + }, // For legacy reasons, we need to populate the below objects. v1: {}, diff --git a/services/web/package-lock.json b/services/web/package-lock.json index 027d0d5cb1..b025d8bbaf 100644 --- a/services/web/package-lock.json +++ b/services/web/package-lock.json @@ -25173,6 +25173,15 @@ "path-exists": "^3.0.0" } }, + "lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dev": true, + "requires": { + "signal-exit": "^3.0.2" + } + }, "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", @@ -25218,6 +25227,12 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", + "dev": true + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -37737,6 +37752,48 @@ } } }, + "webpack-assets-manifest": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/webpack-assets-manifest/-/webpack-assets-manifest-4.0.6.tgz", + "integrity": "sha512-9MsBOINUoGcj3D7XHQOOuQri7VEDArkhn5gqnpCqPungLj8Vy3utlVZ6vddAVU5feYroj+DEncktbaZhnBxdeQ==", + "dev": true, + "requires": { + "chalk": "^4.0", + "deepmerge": "^4.0", + "lockfile": "^1.0", + "lodash.get": "^4.0", + "lodash.has": "^4.0", + "mkdirp": "^1.0", + "schema-utils": "^3.0", + "tapable": "^1.0", + "webpack-sources": "^1.0" + }, + "dependencies": { + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "webpack-cli": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", @@ -38315,31 +38372,6 @@ } } }, - "webpack-manifest-plugin": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz", - "integrity": "sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ==", - "dev": true, - "requires": { - "fs-extra": "^7.0.0", - "lodash": ">=3.5 <5", - "object.entries": "^1.1.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - } - } - }, "webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", diff --git a/services/web/package.json b/services/web/package.json index 422324178e..c09f5caa8c 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -256,9 +256,9 @@ "to-string-loader": "^1.1.6", "val-loader": "^1.1.1", "webpack": "^4.44.2", + "webpack-assets-manifest": "^4.0.6", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0", - "webpack-manifest-plugin": "^2.2.0", "webpack-merge": "^4.2.2" } } diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 21f410f9e9..3101d087cf 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -2,7 +2,7 @@ const fs = require('fs') const path = require('path') const webpack = require('webpack') const CopyPlugin = require('copy-webpack-plugin') -const ManifestPlugin = require('webpack-manifest-plugin') +const WebpackAssetsManifest = require('webpack-assets-manifest') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const PackageVersions = require('./app/src/infrastructure/PackageVersions') @@ -42,6 +42,8 @@ module.exports = { output: { path: path.join(__dirname, '/public'), + publicPath: '/', + // By default write into js directory filename: 'js/[name].js', @@ -250,11 +252,9 @@ module.exports = { plugins: [ // Generate a manifest.json file which is used by the backend to map the // base filenames to the generated output filenames - new ManifestPlugin({ - // Always write the manifest file to disk (even if in dev mode, where - // files are held in memory). This is needed because the server will read - // this file (from disk) when building the script's url - writeToFileEmit: true, + new WebpackAssetsManifest({ + entrypoints: true, + publicPath: true, }), // Prevent moment from loading (very large) locale files that aren't used