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
This commit is contained in:
Jakob Ackermann 2021-09-08 10:24:14 +02:00 committed by Copybot
parent e687230f8a
commit a1c74f27d9
8 changed files with 147 additions and 95 deletions

View file

@ -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()

View file

@ -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'

View file

@ -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

View file

@ -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)

View file

@ -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: {},

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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