diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 0fb34d6b..1db0bfe0 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -75,6 +75,7 @@ class WebpackConfig { this.configuredFilenames = {}; this.aliases = {}; this.externals = []; + this.useIntegrity = false; this.integrityAlgorithms = []; this.shouldUseSingleRuntimeChunk = null; this.shouldSplitEntryChunks = false; @@ -1067,6 +1068,7 @@ class WebpackConfig { } } + this.useIntegrity = enabled; this.integrityAlgorithms = enabled ? algorithms : []; } diff --git a/lib/config-generator.js b/lib/config-generator.js index 0b27eb86..6ffda8a3 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -262,7 +262,8 @@ class ConfigGenerator { // will use the CDN path (if one is available) so that split // chunks load internally through the CDN. publicPath: this.webpackConfig.getRealPublicPath(), - pathinfo: !this.webpackConfig.isProduction() + pathinfo: !this.webpackConfig.isProduction(), + crossOriginLoading: this.webpackConfig.useIntegrity ? 'anonymous' : false, }; } @@ -274,7 +275,33 @@ class ConfigGenerator { return false; } - return applyOptionsCallback(this.webpackConfig.cleanOptionsCallback, {}); + const cleanConfig = applyOptionsCallback(this.webpackConfig.cleanOptionsCallback, {}); + + // TODO: add tests for this + const cleanConfigKeep = cleanConfig.keep; + if (cleanConfigKeep) { + cleanConfig.keep = function (asset) { + // We always want to keep the entrypoints.json file, as it's used by the Webpack Encore Bundle. + if (asset === 'entrypoints.json') { + return true; + } + + if (typeof cleanConfigKeep === 'function') { + return cleanConfigKeep(asset); + } else if (typeof cleanConfigKeep === 'string') { + return asset === cleanConfigKeep; + } else if (cleanConfigKeep instanceof RegExp) { + return cleanConfigKeep.test(asset); + } + + return false; + } + } else { + // We always want to keep the entrypoints.json file, as it's used by the Webpack Encore Bundle. + cleanConfig.keep = 'entrypoints.json'; + } + + return cleanConfig; } buildRulesConfig() { @@ -558,6 +585,10 @@ class ConfigGenerator { splitChunks ); + if (this.webpackConfig.useIntegrity > 0) { + optimization.realContentHash = true; + } + return optimization; } diff --git a/lib/features.js b/lib/features.js index fc0a1822..3c893cd0 100644 --- a/lib/features.js +++ b/lib/features.js @@ -160,6 +160,13 @@ const features = { ], description: 'run the Webpack development server' }, + integrity: { + method: 'enableIntegrityHashes()', + packages: [ + { name: 'webpack-subresource-integrity', enforce_version: true } + ], + description: 'enable Subresource Integrity hashes' + }, }; function getFeatureConfig(featureName) { diff --git a/lib/plugins/entry-files-manifest.js b/lib/plugins/entry-files-manifest.js index 4fbe1a66..71526d54 100644 --- a/lib/plugins/entry-files-manifest.js +++ b/lib/plugins/entry-files-manifest.js @@ -18,71 +18,7 @@ const copyEntryTmpName = require('../utils/copyEntryTmpName'); const AssetsPlugin = require('assets-webpack-plugin'); const fs = require('fs'); const path = require('path'); -const crypto = require('crypto'); - -function processOutput(webpackConfig) { - return (assets) => { - // Remove temporary entry added by the copyFiles feature - delete assets[copyEntryTmpName]; - - // with --watch or dev-server, subsequent calls will include - // the original assets (so, assets.entrypoints) + the new - // assets (which will have their original structure). We - // delete the entrypoints key, and then process the new assets - // like normal below. The same reasoning applies to the - // integrity key. - delete assets.entrypoints; - delete assets.integrity; - - // This will iterate over all the entry points and convert the - // one file entries into an array of one entry since that was how the entry point file was before this change. - const integrity = {}; - const integrityAlgorithms = webpackConfig.integrityAlgorithms; - const publicPath = webpackConfig.getRealPublicPath(); - - for (const asset in assets) { - for (const fileType in assets[asset]) { - if (!Array.isArray(assets[asset][fileType])) { - assets[asset][fileType] = [assets[asset][fileType]]; - } - - if (integrityAlgorithms.length) { - for (const file of assets[asset][fileType]) { - if (file in integrity) { - continue; - } - - const filePath = path.resolve( - webpackConfig.outputPath, - file.replace(publicPath, '') - ); - - if (fs.existsSync(filePath)) { - const fileHashes = []; - - for (const algorithm of webpackConfig.integrityAlgorithms) { - const hash = crypto.createHash(algorithm); - const fileContent = fs.readFileSync(filePath, 'utf8'); - hash.update(fileContent, 'utf8'); - - fileHashes.push(`${algorithm}-${hash.digest('base64')}`); - } - - integrity[file] = fileHashes.join(' '); - } - } - } - } - } - - const manifestContent = { entrypoints: assets }; - if (integrityAlgorithms.length) { - manifestContent.integrity = integrity; - } - - return JSON.stringify(manifestContent, null, 2); - }; -} +const featuresHelper = require("../features"); /** * @param {Array} plugins @@ -90,14 +26,31 @@ function processOutput(webpackConfig) { * @returns {void} */ module.exports = function(plugins, webpackConfig) { + if (webpackConfig.useIntegrity) { + featuresHelper.ensurePackagesExistAndAreCorrectVersion('integrity') + } + plugins.push({ plugin: new AssetsPlugin({ path: webpackConfig.outputPath, filename: 'entrypoints.json', includeAllFileTypes: true, entrypoints: true, - processOutput: processOutput(webpackConfig) + integrity: webpackConfig.useIntegrity, + prettyPrint: true, }), priority: PluginPriorities.AssetsPlugin }); + + if (webpackConfig.useIntegrity) { + const {SubresourceIntegrityPlugin} = require('webpack-subresource-integrity'); + + plugins.push({ + plugin: new SubresourceIntegrityPlugin({ + hashFuncNames: webpackConfig.integrityAlgorithms, + enabled: true, + }), + priority: PluginPriorities.SubresourceIntegrityPlugin + }); + } }; diff --git a/lib/plugins/plugin-priorities.js b/lib/plugins/plugin-priorities.js index 89a4d6c7..fe4faf28 100644 --- a/lib/plugins/plugin-priorities.js +++ b/lib/plugins/plugin-priorities.js @@ -22,4 +22,5 @@ module.exports = { AssetOutputDisplayPlugin: 30, ForkTsCheckerWebpackPlugin: 10, AssetsPlugin: -10, + SubresourceIntegrityPlugin: -20, }; diff --git a/package.json b/package.json index 58647bcc..c24e1283 100755 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "homepage": "https://github.com/symfony/webpack-encore", "dependencies": { "@nuxt/friendly-errors-webpack-plugin": "^2.5.1", - "assets-webpack-plugin": "7.0.*", + "assets-webpack-plugin": "^7.1.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.0", "css-minimizer-webpack-plugin": "^7.0.0", @@ -97,7 +97,8 @@ "webpack": "^5.72", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", - "webpack-notifier": "^1.15.0" + "webpack-notifier": "^1.15.0", + "webpack-subresource-integrity": "^5.1.0" }, "peerDependencies": { "@babel/core": "^7.17.0", @@ -216,6 +217,9 @@ }, "webpack-notifier": { "optional": true + }, + "webpack-subresource-integrity": { + "optional": true } }, "files": [ diff --git a/yarn.lock b/yarn.lock index 2341150a..6c40870e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2091,14 +2091,14 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -assets-webpack-plugin@7.0.*: - version "7.0.0" - resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-7.0.0.tgz#c61ed7466f35ff7a4d90d7070948736f471b8804" - integrity sha512-DMZ9r6HFxynWeONRMhSOFTvTrmit5dovdoUKdJgCG03M6CC7XiwNImPH+Ad1jaVrQ2n59e05lBhte52xPt4MSA== +assets-webpack-plugin@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-7.1.1.tgz#0b988bf904a1895cae5820957ad82aa402673894" + integrity sha512-HwsDcu9UR9kv7AtiyMpUO9fARn94SbrLzw5+aQ59RnOZJeet+EVHmOrMwXl8fZ8cZmdZ9Sbl1/l+fn7ymiyfMg== dependencies: camelcase "^6.0.0" escape-string-regexp "^4.0.0" - lodash "^4.17.20" + lodash "^4.17.21" ast-types@^0.13.4: version "0.13.4" @@ -6927,6 +6927,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-assert@^1.0.8: + version "1.0.9" + resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" + integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg== + typed-query-selector@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" @@ -7177,6 +7182,13 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-subresource-integrity@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz#8b7606b033c6ccac14e684267cb7fb1f5c2a132a" + integrity sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q== + dependencies: + typed-assert "^1.0.8" + webpack@^5.72: version "5.94.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f"