diff --git a/packages/webpack/src/config/client.js b/packages/webpack/src/config/client.js index 231b143f5d..d2126cb035 100644 --- a/packages/webpack/src/config/client.js +++ b/packages/webpack/src/config/client.js @@ -23,6 +23,14 @@ export default class WebpackClientConfig extends WebpackBaseConfig { return this.dev ? 'cheap-module-eval-source-map' : false } + getCspScriptPolicy () { + const { csp } = this.buildContext.options.render + if (csp) { + const { policies = {} } = csp + return policies['script-src'] || policies['default-src'] || [] + } + } + getFileName (...args) { if (this.buildContext.buildOptions.analyze) { const [key] = args @@ -144,9 +152,12 @@ export default class WebpackClientConfig extends WebpackBaseConfig { } if (modern) { + const scriptPolicy = this.getCspScriptPolicy() + const noUnsafeInline = scriptPolicy && !scriptPolicy.includes('\'unsafe-inline\'') plugins.push(new ModernModePlugin({ targetDir: path.resolve(buildDir, 'dist', 'client'), - isModernBuild: this.isModern + isModernBuild: this.isModern, + noUnsafeInline })) } diff --git a/packages/webpack/src/plugins/vue/modern.js b/packages/webpack/src/plugins/vue/modern.js index 25cb104e78..35b6cb56a7 100644 --- a/packages/webpack/src/plugins/vue/modern.js +++ b/packages/webpack/src/plugins/vue/modern.js @@ -10,9 +10,10 @@ const assetsMap = {} const watcher = new EventEmitter() export default class ModernModePlugin { - constructor ({ targetDir, isModernBuild }) { + constructor ({ targetDir, isModernBuild, noUnsafeInline }) { this.targetDir = targetDir this.isModernBuild = isModernBuild + this.noUnsafeInline = noUnsafeInline } apply (compiler) { @@ -83,18 +84,40 @@ export default class ModernModePlugin { const legacyAssets = (await this.getAssets(fileName)) .filter(a => a.tagName === 'script' && a.attributes) - // inject Safari 10 nomodule fix - data.body.push({ - tagName: 'script', - closeTag: true, - innerHTML: safariNoModuleFix - }) - for (const a of legacyAssets) { a.attributes.nomodule = true data.body.push(a) } + if (this.noUnsafeInline) { + // inject the fix as an external script + const safariFixFilename = 'safari-nomodule-fix.js' + const safariFixPath = legacyAssets[0].attributes.src + .split('/') + .slice(0, -1) + .concat([safariFixFilename]) + .join('/') + + compilation.assets[safariFixFilename] = { + source: () => Buffer.from(safariNoModuleFix), + size: () => Buffer.byteLength(safariNoModuleFix) + } + data.body.push({ + tagName: 'script', + closeTag: true, + attributes: { + src: safariFixPath + } + }) + } else { + // inject Safari 10 nomodule fix + data.body.push({ + tagName: 'script', + closeTag: true, + innerHTML: safariNoModuleFix + }) + } + delete assetsMap[fileName] cb() }) diff --git a/test/dev/modern.spa.test.js b/test/dev/modern.spa.test.js index 46932a428b..6fe6c8ef0a 100644 --- a/test/dev/modern.spa.test.js +++ b/test/dev/modern.spa.test.js @@ -54,6 +54,11 @@ describe('modern client mode (SPA)', () => { expect(response).toContain('') }) + test('should contain safari nomodule fix', async () => { + const { body: response } = await rp(url('/'), { headers: { 'user-agent': modernUA } }) + expect(response).toContain('src="/_nuxt/safari-nomodule-fix.js" crossorigin="use-credentials"') + }) + test('should contain modern http2 pushed resources', async () => { const { headers: { link } } = await rp(url('/'), { headers: { 'user-agent': modernUA } }) expect(link).toEqual([ diff --git a/test/fixtures/modern/nuxt.config.js b/test/fixtures/modern/nuxt.config.js index a11c4f59f2..95877bf72a 100644 --- a/test/fixtures/modern/nuxt.config.js +++ b/test/fixtures/modern/nuxt.config.js @@ -11,6 +11,7 @@ export default { } }, render: { + csp: true, crossorigin: 'use-credentials', http2: { push: true