Skip to content

feat: only needs one bundle if all targets support es module #6419

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 8 commits into from
Apr 16, 2021
8 changes: 4 additions & 4 deletions packages/@vue/babel-preset-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 <script type="module">
targets = getModernTargets(targets)
targets = getModuleTargets(targets)
}

// included-by-default polyfills. These are common polyfills that 3rd party
Expand Down
32 changes: 31 additions & 1 deletion packages/@vue/cli-service/__tests__/modernMode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ test('should inject nomodule-fix script when Safari 10 support is required', asy
let { stdout } = await project.run('vue-cli-service build')
let index = await project.read('dist/index.html')
// should inject Safari 10 nomodule fix as an inline script
const { safariFix } = require('../lib/webpack/ModernModePlugin')
const { safariFix } = require('../lib/webpack/SafariNomoduleFixPlugin')
expect(index).toMatch(`<script>${safariFix}</script>`)

// `--no-unsafe-inline` option
Expand All @@ -130,6 +130,36 @@ test('--no-module', async () => {
expect(files.some(f => /-legacy.js/.test(f))).toBe(false)
})

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(`<script defer="defer" src="/js/${f}"`)
}
})

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()
Expand Down
1 change: 1 addition & 0 deletions packages/@vue/cli-service/bin/vue-cli-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
70 changes: 39 additions & 31 deletions packages/@vue/cli-service/lib/commands/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
modernBuild: 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, {
modernBuild: 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
})
}

Expand All @@ -104,8 +112,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}...`)
Expand All @@ -125,7 +133,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
Expand Down Expand Up @@ -162,7 +170,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
}))
})
Expand Down
32 changes: 19 additions & 13 deletions packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,35 @@ module.exports = (api, args, options) => {
})
}

if (args.module) {
if (process.env.VUE_CLI_MODERN_MODE) {
const ModernModePlugin = require('../../webpack/ModernModePlugin')
if (!args.modernBuild) {
const SafariNomoduleFixPlugin = require('../../webpack/SafariNomoduleFixPlugin')

if (!args.moduleBuild) {
// 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,
isModuleBuild: 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,
isModuleBuild: true
}])
}
}

Expand Down
32 changes: 26 additions & 6 deletions packages/@vue/cli-service/lib/util/targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
const { semver } = require('@vue/cli-shared-utils')
const { default: getTargets } = require('@babel/helper-compilation-targets')

const allModernTargets = getTargets(
// See the result at <https://github.com/babel/babel/blob/v7.13.15/packages/babel-compat-data/data/native-modules.json>
const allModuleTargets = getTargets(
{ esmodules: true },
{ ignoreBrowserslistConfig: true }
)
Expand Down Expand Up @@ -32,19 +33,38 @@ function getIntersectionTargets (targets, constraintTargets) {
return intersection
}

function getModernTargets (targets) {
function getModuleTargets (targets) {
// use the intersection of modern mode browsers and user defined targets config
return getIntersectionTargets(targets, allModernTargets)
return getIntersectionTargets(targets, allModuleTargets)
}

function doAllTargetsSupportModule (targets) {
const browserList = Object.keys(targets)

return browserList.every(browserName => {
if (!allModuleTargets[browserName]) {
return false
}

return semver.gte(
semver.coerce(targets[browserName]),
semver.coerce(allModuleTargets[browserName])
)
})
}

// get browserslist targets in current working directory
const projectTargets = getTargets()
const projectModernTargets = getModernTargets(projectTargets)
const projectModuleTargets = getModuleTargets(projectTargets)
const allProjectTargetsSupportModule = doAllTargetsSupportModule(projectTargets)

module.exports = {
getTargets,
getModernTargets,
getModuleTargets,
getIntersectionTargets,
doAllTargetsSupportModule,

projectTargets,
projectModernTargets
projectModuleTargets,
allProjectTargetsSupportModule
}
2 changes: 1 addition & 1 deletion packages/@vue/cli-service/lib/webpack/DashboardPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 5 additions & 52 deletions packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
const fs = require('fs-extra')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const { semver } = require('@vue/cli-shared-utils')
const { projectModernTargets } = require('../util/targets')

const minSafariVersion = projectModernTargets.safari
const minIOSVersion = projectModernTargets.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, isModuleBuild }) {
this.targetDir = targetDir
this.isModernBuild = isModernBuild
this.unsafeInline = unsafeInline
this.jsDirectory = jsDirectory
this.isModuleBuild = isModuleBuild
}

apply (compiler) {
if (!this.isModernBuild) {
if (!this.isModuleBuild) {
this.applyLegacy(compiler)
} else {
this.applyModern(compiler)
this.applyModule(compiler)
}
}

Expand All @@ -53,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) => {
Expand Down Expand Up @@ -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()
Expand All @@ -129,5 +83,4 @@ class ModernModePlugin {
}
}

ModernModePlugin.safariFix = safariFix
module.exports = ModernModePlugin
Loading