Skip to content

feat: support multi-entry library build #6884

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/@vue/cli-service/__tests__/buildLibMultiEntry.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
jest.setTimeout(40000)

const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')

async function makeProjectMultiEntry (project) {
await project.write('vue.config.js', `
module.exports = {
chainWebpack: config => {
config.output.filename("testLib.[name].js");
config.entryPoints.clear();
config
.entry("foo")
.add("./src/foo.js")
.end();
config
.entry("bar")
.add("./src/bar.js")
.end();
}
}
`)
await project.write('src/foo.js', `
import Vue from 'vue'
new Vue({
el: '#app',
render: h => h('h1', 'Foo')
})
`)
await project.write('src/bar.js', `
import Vue from 'vue'
new Vue({
el: '#app',
render: h => h('h1', 'Bar')
})
`)
}

test('build as lib with multi-entry', async () => {
const project = await create('build-lib-multi-entry', defaultPreset)

await makeProjectMultiEntry(project)

const { stdout } = await project.run('vue-cli-service build --target lib')
expect(stdout).toMatch('Build complete.')

expect(project.has('dist/demo.html')).toBe(true)
expect(project.has('dist/testLib.foo.common.js')).toBe(true)
expect(project.has('dist/testLib.foo.umd.js')).toBe(true)
expect(project.has('dist/testLib.foo.umd.min.js')).toBe(true)
expect(project.has('dist/testLib.bar.common.js')).toBe(true)
expect(project.has('dist/testLib.bar.umd.js')).toBe(true)
expect(project.has('dist/testLib.bar.umd.min.js')).toBe(true)
})
2 changes: 1 addition & 1 deletion packages/@vue/cli-service/lib/commands/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = (api, options) => {
}
}
args.entry = args.entry || args._[0]
if (args.target !== 'app') {
if (!['app', 'lib'].includes(args.target)) {
args.entry = args.entry || 'src/App.vue'
}

Expand Down
101 changes: 62 additions & 39 deletions packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,47 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
process.exit(1)
}

// respect inline entry and filename
if (entry) {
filename = (
filename ||
name ||
(
api.service.pkg.name
? api.service.pkg.name.replace(/^@.+\//, '')
: path.basename(entry).replace(/\.(jsx?|vue)$/, '')
)
)
api.chainWebpack((config) => {
config.entryPoints.clear()
config.entry(filename).add(api.resolve(entry))
})
}

const vueMajor = require('../../util/getVueMajor')(api.getCwd())

const fullEntryPath = api.resolve(entry)
function genConfig (entries, entryFilename, format, postfix = format, genHTML) {
const lastEntry = entries.pop()
const fullEntryPath = api.resolve(lastEntry)

if (!fs.existsSync(fullEntryPath)) {
abort(
`Failed to resolve lib entry: ${entry}${entry === `src/App.vue` ? ' (default)' : ''}. ` +
`Make sure to specify the correct entry file.`
)
}
if (!fs.existsSync(fullEntryPath)) {
abort(
`Failed to resolve lib entry: ${lastEntry}${lastEntry === `src/App.vue` ? ' (default)' : ''}. ` +
`Make sure to specify the correct entry file.`
)
}

const isVueEntry = /\.vue$/.test(entry)
const libName = (
name ||
(
api.service.pkg.name
? api.service.pkg.name.replace(/^@.+\//, '')
: path.basename(entry).replace(/\.(jsx?|vue)$/, '')
const isVueEntry = /\.vue$/.test(lastEntry)
const libName = (
name ||
(
api.service.pkg.name
? api.service.pkg.name.replace(/^@.+\//, '')
: path.basename(lastEntry).replace(/\.(jsx?|vue)$/, '')
)
)
)
filename = filename || libName
function genConfig (format, postfix = format, genHTML) {
const config = api.resolveChainableWebpackConfig()

const config = api.resolveChainableWebpackConfig()
const browserslist = require('browserslist')
const targets = browserslist(undefined, { path: fullEntryPath })
const supportsIE = targets.some(agent => agent.includes('ie'))
Expand All @@ -48,7 +65,7 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
config
.plugin('extract-css')
.tap(args => {
args[0].filename = `${filename}.css`
args[0].filename = `${entryFilename}.css`
return args
})
}
Expand All @@ -69,13 +86,13 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
filename: 'demo.html',
libName,
vueMajor,
assetsFileName: filename,
assetsFileName: entryFilename,
cssExtract: config.plugins.has('extract-css')
}])
}

// resolve entry/output
const entryName = `${filename}.${postfix}`
const entryName = `${entryFilename}.${postfix}`
config.resolve
.alias
.set('~entry', fullEntryPath)
Expand Down Expand Up @@ -110,8 +127,10 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
}
].filter(Boolean)

entries.push(realEntry)

rawConfig.entry = {
[entryName]: realEntry
[entryName]: entries
}

rawConfig.output = Object.assign({
Expand All @@ -122,10 +141,10 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
// libraryTarget: 'esm' or target: 'universal'
// https://github.com/webpack/webpack/issues/6522
// https://github.com/webpack/webpack/issues/6525
globalObject: `(typeof self !== 'undefined' ? self : this)`
}, rawConfig.output, {
globalObject: `(typeof self !== 'undefined' ? self : this)`,
filename: `${entryName}.js`,
chunkFilename: `${entryName}.[name].js`,
chunkFilename: `${entryName}.[name].js`
}, rawConfig.output, {
// use dynamic publicPath so this can be deployed anywhere
// the actual path will be determined at runtime by checking
// document.currentScript.src.
Expand All @@ -140,20 +159,24 @@ module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue
return rawConfig
}

const configMap = {
commonjs: genConfig('commonjs2', 'common'),
umd: genConfig('umd', undefined, true),
'umd-min': genConfig('umd', 'umd.min')
}
const baseConfig = api.resolveChainableWebpackConfig()

const formatArray = (formats + '').split(',')
const configs = formatArray.map(format => configMap[format])
if (configs.indexOf(undefined) !== -1) {
const unknownFormats = formatArray.filter(f => configMap[f] === undefined).join(', ')
abort(
`Unknown library build formats: ${unknownFormats}`
)
}
return Object.entries(baseConfig.entryPoints.entries()).reduce((previousValue, [entryFilename, entries]) => {
const configMap = {
commonjs: genConfig(entries.values(), entryFilename, 'commonjs2', 'common'),
umd: genConfig(entries.values(), entryFilename, 'umd', undefined, true),
'umd-min': genConfig(entries.values(), entryFilename, 'umd', 'umd.min')
}

const formatArray = (formats + '').split(',')
const configs = formatArray.map(format => configMap[format])
if (configs.indexOf(undefined) !== -1) {
const unknownFormats = formatArray.filter(f => configMap[f] === undefined).join(', ')
abort(
`Unknown library build formats: ${unknownFormats}`
)
}

return configs
return previousValue.concat(configs)
}, [])
}