Просмотр исходного кода

Merge pull request #4926 from overleaf/jpa-webpack-dynamic-entrypoint-chunks

[web] get the list of js/css entrypoint chunks from webpack manifest

GitOrigin-RevId: 42a8d3606e461e8d9eebcc754e3207d5be1746ab
Jakob Ackermann 5 лет назад
Родитель
Сommit
a1c74f27d9

+ 67 - 54
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()

+ 5 - 3
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'

+ 3 - 2
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

+ 5 - 4
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)

+ 3 - 0
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: {},

+ 57 - 25
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",

+ 1 - 1
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"
   }
 }

+ 6 - 6
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