From 0996b206314cb5316a2c1317395f606d77cdb88e Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 14 Apr 2021 19:41:11 +0800 Subject: [PATCH 1/6] refactor: rename modern to module in target calculation code As we are getting rid of the name "modern mode" --- packages/@vue/babel-preset-app/index.js | 8 ++++---- packages/@vue/cli-service/lib/util/targets.js | 14 ++++++++------ .../cli-service/lib/webpack/ModernModePlugin.js | 6 +++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/@vue/babel-preset-app/index.js b/packages/@vue/babel-preset-app/index.js index f2f712cc87..51b0865f62 100644 --- a/packages/@vue/babel-preset-app/index.js +++ b/packages/@vue/babel-preset-app/index.js @@ -43,14 +43,14 @@ function getIntersectionTargets (targets, constraintTargets) { return intersection } -function getModernTargets (targets) { - const allModernTargets = getTargets( +function getModuleTargets (targets) { + const allModuleTargets = getTargets( { esmodules: true }, { ignoreBrowserslistConfig: true } ) // use the intersection of modern mode browsers and user defined targets config - return getIntersectionTargets(targets, allModernTargets) + return getIntersectionTargets(targets, allModuleTargets) } function getWCTargets (targets) { @@ -177,7 +177,7 @@ module.exports = (context, options = {}) => { targets = getWCTargets(targets) } else if (process.env.VUE_CLI_MODERN_BUILD) { // targeting browsers that at least support `) // `--no-unsafe-inline` option diff --git a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js index b8f518af4c..28502a2cf1 100644 --- a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js +++ b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js @@ -19,27 +19,33 @@ module.exports = (api, args, options) => { if (args.module) { const ModernModePlugin = require('../../webpack/ModernModePlugin') + const SafariNomoduleFixPlugin = require('../../webpack/SafariNomoduleFixPlugin') + if (!args.modernBuild) { // Inject plugin to extract build stats and write to disk config - .plugin('modern-mode-legacy') - .use(ModernModePlugin, [{ - targetDir, - isModernBuild: false, - unsafeInline: args['unsafe-inline'] - }]) + .plugin('modern-mode-legacy') + .use(ModernModePlugin, [{ + targetDir, + isModernBuild: false + }]) } else { - // Inject plugin to read non-modern build stats and inject HTML config - .plugin('modern-mode-modern') - .use(ModernModePlugin, [{ - targetDir, - isModernBuild: true, + .plugin('safari-nomodule-fix') + .use(SafariNomoduleFixPlugin, [{ unsafeInline: args['unsafe-inline'], // as we may generate an addition file asset (if `no-unsafe-inline` specified) // we need to provide the correct directory for that file to place in jsDirectory: require('../../util/getAssetPath')(options, 'js') }]) + + // Inject plugin to read non-modern build stats and inject HTML + config + .plugin('modern-mode-modern') + .use(ModernModePlugin, [{ + targetDir, + isModernBuild: true + }]) } } diff --git a/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js b/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js index 6c4f046eac..5767ca46a8 100644 --- a/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js +++ b/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js @@ -1,26 +1,10 @@ const fs = require('fs-extra') const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') - -const { semver } = require('@vue/cli-shared-utils') -const { projectModuleTargets } = require('../util/targets') - -const minSafariVersion = projectModuleTargets.safari -const minIOSVersion = projectModuleTargets.ios -const supportsSafari10 = - (minSafariVersion && semver.lt(semver.coerce(minSafariVersion), '11.0.0')) || - (minIOSVersion && semver.lt(semver.coerce(minIOSVersion), '11.0.0')) -const needsSafariFix = supportsSafari10 - -// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc -const safariFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();` - class ModernModePlugin { - constructor ({ targetDir, isModernBuild, unsafeInline, jsDirectory }) { + constructor ({ targetDir, isModernBuild }) { this.targetDir = targetDir this.isModernBuild = isModernBuild - this.unsafeInline = unsafeInline - this.jsDirectory = jsDirectory } apply (compiler) { @@ -87,36 +71,6 @@ class ModernModePlugin { .filter(a => a.tagName === 'script' && a.attributes) legacyAssets.forEach(a => { a.attributes.nomodule = '' }) - if (needsSafariFix) { - if (this.unsafeInline) { - // inject inline Safari 10 nomodule fix - tags.push({ - tagName: 'script', - closeTag: true, - innerHTML: safariFix - }) - } else { - // inject the fix as an external script - const safariFixPath = path.join(this.jsDirectory, 'safari-nomodule-fix.js') - const fullSafariFixPath = path.join(compilation.options.output.publicPath, safariFixPath) - compilation.assets[safariFixPath] = { - source: function () { - return Buffer.from(safariFix) - }, - size: function () { - return Buffer.byteLength(safariFix) - } - } - tags.push({ - tagName: 'script', - closeTag: true, - attributes: { - src: fullSafariFixPath - } - }) - } - } - tags.push(...legacyAssets) await fs.remove(tempFilename) cb() @@ -129,5 +83,4 @@ class ModernModePlugin { } } -ModernModePlugin.safariFix = safariFix module.exports = ModernModePlugin diff --git a/packages/@vue/cli-service/lib/webpack/SafariNomoduleFixPlugin.js b/packages/@vue/cli-service/lib/webpack/SafariNomoduleFixPlugin.js new file mode 100644 index 0000000000..fef45a6416 --- /dev/null +++ b/packages/@vue/cli-service/lib/webpack/SafariNomoduleFixPlugin.js @@ -0,0 +1,75 @@ +// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc +const safariFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();` + +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { semver } = require('@vue/cli-shared-utils') +const { projectModuleTargets } = require('../util/targets') + +const minSafariVersion = projectModuleTargets.safari +const minIOSVersion = projectModuleTargets.ios +const supportsSafari10 = + (minSafariVersion && semver.lt(semver.coerce(minSafariVersion), '11.0.0')) || + (minIOSVersion && semver.lt(semver.coerce(minIOSVersion), '11.0.0')) +const needsSafariFix = supportsSafari10 + +class SafariNomoduleFixPlugin { + constructor ({ unsafeInline, jsDirectory }) { + this.unsafeInline = unsafeInline + this.jsDirectory = jsDirectory + } + + apply (compiler) { + if (!needsSafariFix) { + return + } + + const ID = 'SafariNomoduleFixPlugin' + compiler.hooks.compilation.tap(ID, compilation => { + HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tap(ID, data => { + let scriptTag + + if (this.unsafeInline) { + // inject inline Safari 10 nomodule fix + scriptTag = { + tagName: 'script', + closeTag: true, + innerHTML: safariFix + } + } else { + // inject the fix as an external script + const safariFixPath = path.join(this.jsDirectory, 'safari-nomodule-fix.js') + const fullSafariFixPath = path.join(compilation.options.output.publicPath, safariFixPath) + compilation.assets[safariFixPath] = { + source: function () { + return Buffer.from(safariFix) + }, + size: function () { + return Buffer.byteLength(safariFix) + } + } + scriptTag = { + tagName: 'script', + closeTag: true, + attributes: { + src: fullSafariFixPath + } + } + } + + let tags = data.bodyTags + if (data.plugin.options.scriptLoading === 'defer') { + tags = data.headTags + } + + // insert just before the first actual script tag, + // and after all other tags such as `meta` + const firstScriptIndex = tags.findIndex(tag => tag.tagName === 'script') + tags.splice(firstScriptIndex, 0, scriptTag) + }) + }) + } +} + +SafariNomoduleFixPlugin.safariFix = safariFix +module.exports = SafariNomoduleFixPlugin From abffedef6367bd28ebd038b951947a6d483823bf Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 14 Apr 2021 21:51:58 +0800 Subject: [PATCH 4/6] refactor: rename modernBuild to moduleBuild --- .../@vue/cli-service/lib/commands/build/index.js | 12 ++++++------ .../lib/commands/build/resolveAppConfig.js | 6 +++--- .../@vue/cli-service/lib/webpack/DashboardPlugin.js | 2 +- .../@vue/cli-service/lib/webpack/ModernModePlugin.js | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/@vue/cli-service/lib/commands/build/index.js b/packages/@vue/cli-service/lib/commands/build/index.js index fcad4278d8..5b27f0ac2b 100644 --- a/packages/@vue/cli-service/lib/commands/build/index.js +++ b/packages/@vue/cli-service/lib/commands/build/index.js @@ -58,7 +58,7 @@ module.exports = (api, options) => { if (!process.env.VUE_CLI_MODERN_BUILD) { // main-process for legacy build await build(Object.assign({}, args, { - modernBuild: false, + moduleBuild: false, keepAlive: true }), api, options) // spawn sub-process of self for modern build @@ -73,7 +73,7 @@ module.exports = (api, options) => { } else { // sub-process for modern build await build(Object.assign({}, args, { - modernBuild: true, + moduleBuild: true, clean: false }), api, options) } @@ -104,8 +104,8 @@ async function build (args, api, options) { const mode = api.service.mode if (args.target === 'app') { const bundleTag = args.module - ? args.modernBuild - ? `modern bundle ` + ? args.moduleBuild + ? `module bundle ` : `legacy bundle ` : `` logWithSpinner(`Building ${bundleTag}for ${mode}...`) @@ -125,7 +125,7 @@ async function build (args, api, options) { } const targetDir = api.resolve(options.outputDir) - const isLegacyBuild = args.target === 'app' && args.module && !args.modernBuild + const isLegacyBuild = args.target === 'app' && args.module && !args.moduleBuild // resolve raw webpack config let webpackConfig @@ -162,7 +162,7 @@ async function build (args, api, options) { modifyConfig(webpackConfig, config => { config.plugins.push(new DashboardPlugin({ type: 'build', - modernBuild: args.modernBuild, + moduleBuild: args.moduleBuild, keepAlive: args.keepAlive })) }) diff --git a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js index 28502a2cf1..dc378aa1b9 100644 --- a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js +++ b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js @@ -21,13 +21,13 @@ module.exports = (api, args, options) => { const ModernModePlugin = require('../../webpack/ModernModePlugin') const SafariNomoduleFixPlugin = require('../../webpack/SafariNomoduleFixPlugin') - if (!args.modernBuild) { + if (!args.moduleBuild) { // Inject plugin to extract build stats and write to disk config .plugin('modern-mode-legacy') .use(ModernModePlugin, [{ targetDir, - isModernBuild: false + isModuleBuild: false }]) } else { config @@ -44,7 +44,7 @@ module.exports = (api, args, options) => { .plugin('modern-mode-modern') .use(ModernModePlugin, [{ targetDir, - isModernBuild: true + isModuleBuild: true }]) } } diff --git a/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js index 3364e1ffab..439e0be353 100644 --- a/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js +++ b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js @@ -33,7 +33,7 @@ function getTimeMessage (timer) { class DashboardPlugin { constructor (options) { this.type = options.type - if (this.type === 'build' && options.modernBuild) { + if (this.type === 'build' && options.moduleBuild) { this.type = 'build-modern' } this.watching = false diff --git a/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js b/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js index 5767ca46a8..e6632b4417 100644 --- a/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js +++ b/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js @@ -2,16 +2,16 @@ const fs = require('fs-extra') const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') class ModernModePlugin { - constructor ({ targetDir, isModernBuild }) { + constructor ({ targetDir, isModuleBuild }) { this.targetDir = targetDir - this.isModernBuild = isModernBuild + this.isModuleBuild = isModuleBuild } apply (compiler) { - if (!this.isModernBuild) { + if (!this.isModuleBuild) { this.applyLegacy(compiler) } else { - this.applyModern(compiler) + this.applyModule(compiler) } } @@ -37,7 +37,7 @@ class ModernModePlugin { }) } - applyModern (compiler) { + applyModule (compiler) { const ID = `vue-cli-modern-bundle` compiler.hooks.compilation.tap(ID, compilation => { HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync(ID, async (data, cb) => { From 1d7a1c895edb2dc269f0ea86fbaf19005068ddd6 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Thu, 15 Apr 2021 10:16:01 +0800 Subject: [PATCH 5/6] feat: only needs one bundle if all targets support es module --- .../cli-service/__tests__/modernMode.spec.js | 19 ++++++ .../@vue/cli-service/bin/vue-cli-service.js | 1 + .../cli-service/lib/commands/build/index.js | 62 +++++++++++-------- .../lib/commands/build/resolveAppConfig.js | 2 +- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/packages/@vue/cli-service/__tests__/modernMode.spec.js b/packages/@vue/cli-service/__tests__/modernMode.spec.js index 73789e356f..856ed20294 100644 --- a/packages/@vue/cli-service/__tests__/modernMode.spec.js +++ b/packages/@vue/cli-service/__tests__/modernMode.spec.js @@ -130,6 +130,25 @@ test('--no-module', async () => { expect(files.some(f => /-legacy.js/.test(f))).toBe(false) }) +test.todo('should use correct hash for fallback bundles') + +test('should only build one bundle if all targets support ES module', async () => { + const project = await create('no-differential-loading', defaultPreset) + + const pkg = JSON.parse(await project.read('package.json')) + pkg.browserslist.push('not ie <= 11') + await project.write('package.json', JSON.stringify(pkg, null, 2)) + + const { stdout } = await project.run('vue-cli-service build') + expect(stdout).toMatch('Build complete.') + + const index = await project.read('dist/index.html') + expect(index).not.toMatch('type="module"') + + const files = await fs.readdir(path.join(project.dir, 'dist/js')) + expect(files.some(f => /-legacy.js/.test(f))).toBe(false) +}) + afterAll(async () => { if (browser) { await browser.close() diff --git a/packages/@vue/cli-service/bin/vue-cli-service.js b/packages/@vue/cli-service/bin/vue-cli-service.js index adfe811c3c..126acecab9 100755 --- a/packages/@vue/cli-service/bin/vue-cli-service.js +++ b/packages/@vue/cli-service/bin/vue-cli-service.js @@ -18,6 +18,7 @@ const rawArgv = process.argv.slice(2) const args = require('minimist')(rawArgv, { boolean: [ // build + // FIXME: --no-module, --no-unsafe-inline, no-clean, etc. 'modern', 'report', 'report-json', diff --git a/packages/@vue/cli-service/lib/commands/build/index.js b/packages/@vue/cli-service/lib/commands/build/index.js index 5b27f0ac2b..0b5ff894cd 100644 --- a/packages/@vue/cli-service/lib/commands/build/index.js +++ b/packages/@vue/cli-service/lib/commands/build/index.js @@ -53,35 +53,43 @@ module.exports = (api, options) => { } process.env.VUE_CLI_BUILD_TARGET = args.target - if (args.module && args.target === 'app') { - process.env.VUE_CLI_MODERN_MODE = true - if (!process.env.VUE_CLI_MODERN_BUILD) { - // main-process for legacy build - await build(Object.assign({}, args, { - moduleBuild: false, - keepAlive: true - }), api, options) - // spawn sub-process of self for modern build - const { execa } = require('@vue/cli-shared-utils') - const cliBin = require('path').resolve(__dirname, '../../../bin/vue-cli-service.js') - await execa('node', [cliBin, 'build', ...rawArgs], { - stdio: 'inherit', - env: { - VUE_CLI_MODERN_BUILD: true - } - }) - } else { - // sub-process for modern build - await build(Object.assign({}, args, { - moduleBuild: true, - clean: false - }), api, options) - } - delete process.env.VUE_CLI_MODERN_MODE - } else { + + const { log, execa } = require('@vue/cli-shared-utils') + const { allProjectTargetsSupportModule } = require('../../util/targets') + + let needsDifferentialLoading = args.target === 'app' && args.module + if (allProjectTargetsSupportModule) { + log( + `All browser targets in the browserslist configuration have supported ES module.\n` + + `Therefore we don't build two separate bundles for differential loading.\n` + ) + needsDifferentialLoading = false + } + + if (!needsDifferentialLoading) { await build(args, api, options) + return + } + + process.env.VUE_CLI_MODERN_MODE = true + if (!process.env.VUE_CLI_MODERN_BUILD) { + // main-process for legacy build + const legacyBuildArgs = { ...args, moduleBuild: false, keepAlive: true } + await build(legacyBuildArgs, api, options) + + // spawn sub-process of self for modern build + const cliBin = require('path').resolve(__dirname, '../../../bin/vue-cli-service.js') + await execa('node', [cliBin, 'build', ...rawArgs], { + stdio: 'inherit', + env: { + VUE_CLI_MODERN_BUILD: true + } + }) + } else { + // sub-process for modern build + const moduleBuildArgs = { ...args, moduleBuild: true, clean: false } + await build(moduleBuildArgs, api, options) } - delete process.env.VUE_CLI_BUILD_TARGET }) } diff --git a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js index dc378aa1b9..3ab09b3000 100644 --- a/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js +++ b/packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js @@ -17,7 +17,7 @@ module.exports = (api, args, options) => { }) } - if (args.module) { + if (process.env.VUE_CLI_MODERN_MODE) { const ModernModePlugin = require('../../webpack/ModernModePlugin') const SafariNomoduleFixPlugin = require('../../webpack/SafariNomoduleFixPlugin') From eef27c34b9b7b5b97b244cba11b79d44509ac485 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Thu, 15 Apr 2021 10:36:12 +0800 Subject: [PATCH 6/6] test: add test case for fallback bundle hashes in html --- .../@vue/cli-service/__tests__/modernMode.spec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/@vue/cli-service/__tests__/modernMode.spec.js b/packages/@vue/cli-service/__tests__/modernMode.spec.js index 856ed20294..1be69c300d 100644 --- a/packages/@vue/cli-service/__tests__/modernMode.spec.js +++ b/packages/@vue/cli-service/__tests__/modernMode.spec.js @@ -130,7 +130,18 @@ test('--no-module', async () => { expect(files.some(f => /-legacy.js/.test(f))).toBe(false) }) -test.todo('should use correct hash for fallback bundles') +test('should use correct hash for fallback bundles', async () => { + const project = await create('legacy-hash', defaultPreset) + + const { stdout } = await project.run('vue-cli-service build') + expect(stdout).toMatch('Build complete.') + + const index = await project.read('dist/index.html') + const jsFiles = (await fs.readdir(path.join(project.dir, 'dist/js'))).filter(f => f.endsWith('.js')) + for (const f of jsFiles) { + expect(index).toMatch(`