first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title><%- htmlWebpackPlugin.options.libName %> demo</title>
<script src="./<%- htmlWebpackPlugin.options.assetsFileName %>.umd.js"></script>
<% if (htmlWebpackPlugin.options.cssExtract) { %>
<link rel="stylesheet" href="./<%- htmlWebpackPlugin.options.assetsFileName %>.css">
<% } %>
<script>
console.log(<%- htmlWebpackPlugin.options.libName %>)
</script>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title><%- htmlWebpackPlugin.options.libName %> demo</title>
<script src="//unpkg.com/vue@<%- htmlWebpackPlugin.options.vueMajor %>"></script>
<script src="./<%- htmlWebpackPlugin.options.assetsFileName %>.umd.js"></script>
<% if (htmlWebpackPlugin.options.cssExtract) { %>
<link rel="stylesheet" href="./<%- htmlWebpackPlugin.options.assetsFileName %>.css">
<% } %>
<div id="app">
<demo></demo>
</div>
<script>
<% if (htmlWebpackPlugin.options.vueMajor === 3) { %>
Vue.createApp({
components: {
demo: <%- htmlWebpackPlugin.options.libName %>
}
}).mount('#app')
<% } else { %>
new Vue({
components: {
demo: <%- htmlWebpackPlugin.options.libName %>
}
}).$mount('#app')
<% } %>
</script>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title><%- htmlWebpackPlugin.options.libName %> demo</title>
<script src="https://unpkg.com/vue@<%- htmlWebpackPlugin.options.vueMajor %>"></script>
<script src="./<%- htmlWebpackPlugin.options.libName %>.js"></script>
<% for (const comp of htmlWebpackPlugin.options.components) { %>
<<%= comp %>></<%= comp %>>
<% } %>

View File

@ -0,0 +1,2 @@
import './setPublicPath'
export * from '~entry'

View File

@ -0,0 +1,4 @@
import './setPublicPath'
import mod from '~entry'
export default mod
export * from '~entry'

View File

@ -0,0 +1,12 @@
import './setPublicPath'
import Vue from 'vue'
import wrap from '@vue/web-component-wrapper'
// runtime shared by every component chunk
import 'css-loader/dist/runtime/api.js'
import 'vue-style-loader/lib/addStylesShadow'
import 'vue-loader/lib/runtime/componentNormalizer'
window.customElements.define('build-wc-async-app', wrap(Vue, () => import('~root/src/App.vue?shadow')))
window.customElements.define('build-wc-async-hello-world', wrap(Vue, () => import('~root/src/components/HelloWorld.vue?shadow')))

View File

@ -0,0 +1,77 @@
module.exports = function formatStats (stats, dir, api) {
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const ui = require('cliui')({ width: process.stdout.columns || 80 })
const { chalk } = require('@vue/cli-shared-utils')
const json = stats.toJson({
hash: false,
modules: false,
chunks: false
})
let assets = json.assets
? json.assets
: json.children.reduce((acc, child) => acc.concat(child.assets), [])
const seenNames = new Map()
const isJS = val => /\.js$/.test(val)
const isCSS = val => /\.css$/.test(val)
const isMinJS = val => /\.min\.js$/.test(val)
assets = assets
.map(a => {
a.name = a.name.split('?')[0]
return a
})
.filter(a => {
if (seenNames.has(a.name)) {
return false
}
seenNames.set(a.name, true)
return isJS(a.name) || isCSS(a.name)
})
.sort((a, b) => {
if (isJS(a.name) && isCSS(b.name)) return -1
if (isCSS(a.name) && isJS(b.name)) return 1
if (isMinJS(a.name) && !isMinJS(b.name)) return -1
if (!isMinJS(a.name) && isMinJS(b.name)) return 1
return b.size - a.size
})
function formatSize (size) {
return (size / 1024).toFixed(2) + ' KiB'
}
function getGzippedSize (asset) {
const filepath = api.resolve(path.join(dir, asset.name))
const buffer = fs.readFileSync(filepath)
return formatSize(zlib.gzipSync(buffer).length)
}
function makeRow (a, b, c) {
return ` ${a}\t ${b}\t ${c}`
}
ui.div(
makeRow(
chalk.cyan.bold(`File`),
chalk.cyan.bold(`Size`),
chalk.cyan.bold(`Gzipped`)
) + `\n\n` +
assets.map(asset => makeRow(
/js$/.test(asset.name)
? chalk.green(path.join(dir, asset.name))
: chalk.blue(path.join(dir, asset.name)),
formatSize(asset.size),
getGzippedSize(asset)
)).join(`\n`)
)
const time = stats.endTime - stats.startTime
const now = new Date().toISOString()
const hash = stats.hash
const info = `Build at: ${chalk.white(now)} - Hash: ${chalk.white(hash)} - Time: ${chalk.white(time)}ms`
return `${ui.toString()}\n\n ${chalk.gray(`Images and other types of assets omitted.`)}\n ${info}\n`
}

View File

@ -0,0 +1,238 @@
const defaults = {
clean: true,
target: 'app',
module: true,
formats: 'commonjs,umd,umd-min'
}
const buildModes = {
lib: 'library',
wc: 'web component',
'wc-async': 'web component (async)'
}
const modifyConfig = (config, fn) => {
if (Array.isArray(config)) {
config.forEach(c => fn(c))
} else {
fn(config)
}
}
module.exports = (api, options) => {
api.registerCommand('build', {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
'--mode': `specify env mode (default: production)`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--no-module': `build app without generating <script type="module"> chunks for modern browsers`,
'--target': `app | lib | wc | wc-async (default: ${defaults.target})`,
'--inline-vue': 'include the Vue module in the final bundle of library or web component target',
'--formats': `list of output formats for library builds (default: ${defaults.formats})`,
'--name': `name for lib or web-component mode (default: "name" in package.json or entry filename)`,
'--filename': `file name for output, only usable for 'lib' target (default: value of --name)`,
'--no-clean': `do not remove the dist directory contents before building the project`,
'--report': `generate report.html to help analyze bundle content`,
'--report-json': 'generate report.json to help analyze bundle content',
'--skip-plugins': `comma-separated list of plugin names to skip for this run`,
'--watch': `watch for changes`,
'--stdin': `close when stdin ends`
}
}, async (args, rawArgs) => {
for (const key in defaults) {
if (args[key] == null) {
args[key] = defaults[key]
}
}
args.entry = args.entry || args._[0]
if (args.target !== 'app') {
args.entry = args.entry || 'src/App.vue'
}
process.env.VUE_CLI_BUILD_TARGET = args.target
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
}
args.needsDifferentialLoading = needsDifferentialLoading
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)
}
})
}
async function build (args, api, options) {
const fs = require('fs-extra')
const path = require('path')
const webpack = require('webpack')
const { chalk } = require('@vue/cli-shared-utils')
const formatStats = require('./formatStats')
const validateWebpackConfig = require('../../util/validateWebpackConfig')
const {
log,
done,
info,
logWithSpinner,
stopSpinner
} = require('@vue/cli-shared-utils')
log()
const mode = api.service.mode
if (args.target === 'app') {
const bundleTag = args.needsDifferentialLoading
? args.moduleBuild
? `module bundle `
: `legacy bundle `
: ``
logWithSpinner(`Building ${bundleTag}for ${mode}...`)
} else {
const buildMode = buildModes[args.target]
if (buildMode) {
const additionalParams = buildMode === 'library' ? ` (${args.formats})` : ``
logWithSpinner(`Building for ${mode} as ${buildMode}${additionalParams}...`)
} else {
throw new Error(`Unknown build target: ${args.target}`)
}
}
if (args.dest) {
// Override outputDir before resolving webpack config as config relies on it (#2327)
options.outputDir = args.dest
}
const targetDir = api.resolve(options.outputDir)
const isLegacyBuild = args.needsDifferentialLoading && !args.moduleBuild
// resolve raw webpack config
let webpackConfig
if (args.target === 'lib') {
webpackConfig = require('./resolveLibConfig')(api, args, options)
} else if (
args.target === 'wc' ||
args.target === 'wc-async'
) {
webpackConfig = require('./resolveWcConfig')(api, args, options)
} else {
webpackConfig = require('./resolveAppConfig')(api, args, options)
}
// check for common config errors
validateWebpackConfig(webpackConfig, api, options, args.target)
if (args.watch) {
modifyConfig(webpackConfig, config => {
config.watch = true
})
}
if (args.stdin) {
process.stdin.on('end', () => {
process.exit(0)
})
process.stdin.resume()
}
// Expose advanced stats
if (args.dashboard) {
const DashboardPlugin = require('../../webpack/DashboardPlugin')
modifyConfig(webpackConfig, config => {
config.plugins.push(new DashboardPlugin({
type: 'build',
moduleBuild: args.moduleBuild,
keepAlive: args.keepAlive
}))
})
}
if (args.report || args['report-json']) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
modifyConfig(webpackConfig, config => {
const bundleName = args.target !== 'app'
? config.output.filename.replace(/\.js$/, '-')
: isLegacyBuild ? 'legacy-' : ''
config.plugins.push(new BundleAnalyzerPlugin({
logLevel: 'warn',
openAnalyzer: false,
analyzerMode: args.report ? 'static' : 'disabled',
reportFilename: `${bundleName}report.html`,
statsFilename: `${bundleName}report.json`,
generateStatsFile: !!args['report-json']
}))
})
}
if (args.clean) {
await fs.emptyDir(targetDir)
}
return new Promise((resolve, reject) => {
webpack(webpackConfig, (err, stats) => {
stopSpinner(false)
if (err) {
return reject(err)
}
if (stats.hasErrors()) {
return reject(new Error('Build failed with errors.'))
}
if (!args.silent) {
const targetDirShort = path.relative(
api.service.context,
targetDir
)
log(formatStats(stats, targetDirShort, api))
if (args.target === 'app' && !isLegacyBuild) {
if (!args.watch) {
done(`Build complete. The ${chalk.cyan(targetDirShort)} directory is ready to be deployed.`)
info(`Check out deployment instructions at ${chalk.cyan(`https://cli.vuejs.org/guide/deployment.html`)}\n`)
} else {
done(`Build complete. Watching for changes...`)
}
}
}
// test-only signal
if (process.env.VUE_CLI_TEST) {
console.log('Build complete.')
}
resolve()
})
})
}
module.exports.defaultModes = {
build: 'production'
}

View File

@ -0,0 +1,52 @@
module.exports = (api, args, options) => {
// respect inline entry
if (args.entry && !options.pages) {
api.configureWebpack(config => {
config.entry = { app: api.resolve(args.entry) }
})
}
const config = api.resolveChainableWebpackConfig()
const targetDir = api.resolve(args.dest || options.outputDir)
// respect inline build destination in copy plugin
if (args.dest && config.plugins.has('copy')) {
config.plugin('copy').tap(pluginArgs => {
pluginArgs[0].patterns.to = targetDir
return pluginArgs
})
}
if (process.env.VUE_CLI_MODERN_MODE) {
const ModernModePlugin = require('../../webpack/ModernModePlugin')
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,
isModuleBuild: false
}])
} else {
config
.plugin('safari-nomodule-fix')
.use(SafariNomoduleFixPlugin, [{
// as we may generate an addition file asset (if Safari 10 fix is needed)
// 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
}])
}
}
return api.resolveWebpackConfig(config)
}

View File

@ -0,0 +1,159 @@
const fs = require('fs')
const path = require('path')
module.exports = (api, { entry, name, formats, filename, 'inline-vue': inlineVue }, options) => {
const { log, error } = require('@vue/cli-shared-utils')
const abort = msg => {
log()
error(msg)
process.exit(1)
}
const vueMajor = require('../../util/getVueMajor')(api.getCwd())
const fullEntryPath = api.resolve(entry)
if (!fs.existsSync(fullEntryPath)) {
abort(
`Failed to resolve lib entry: ${entry}${entry === `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)$/, '')
)
)
filename = filename || libName
function genConfig (format, postfix = format, genHTML) {
const config = api.resolveChainableWebpackConfig()
const browserslist = require('browserslist')
const targets = browserslist(undefined, { path: fullEntryPath })
const supportsIE = targets.some(agent => agent.includes('ie'))
const webpack = require('webpack')
config.plugin('need-current-script-polyfill')
.use(webpack.DefinePlugin, [{
'process.env.NEED_CURRENTSCRIPT_POLYFILL': JSON.stringify(supportsIE)
}])
// adjust css output name so they write to the same file
if (config.plugins.has('extract-css')) {
config
.plugin('extract-css')
.tap(args => {
args[0].filename = `${filename}.css`
return args
})
}
// only minify min entry
if (!/\.min/.test(postfix)) {
config.optimization.minimize(false)
}
// inject demo page for umd
if (genHTML) {
const template = isVueEntry ? 'demo-lib.html' : 'demo-lib-js.html'
config
.plugin('demo-html')
.use(require('html-webpack-plugin'), [{
template: path.resolve(__dirname, template),
inject: false,
filename: 'demo.html',
libName,
vueMajor,
assetsFileName: filename,
cssExtract: config.plugins.has('extract-css')
}])
}
// resolve entry/output
const entryName = `${filename}.${postfix}`
config.resolve
.alias
.set('~entry', fullEntryPath)
// set output target before user configureWebpack hooks are applied
config.output.libraryTarget(format)
// set entry/output after user configureWebpack hooks are applied
const rawConfig = api.resolveWebpackConfig(config)
let realEntry = require.resolve('./entry-lib.js')
// avoid importing default if user entry file does not have default export
if (!isVueEntry) {
const entryContent = fs.readFileSync(fullEntryPath, 'utf-8')
if (!/\b(export\s+default|export\s{[^}]+as\s+default)\b/.test(entryContent)) {
realEntry = require.resolve('./entry-lib-no-default.js')
}
}
// externalize Vue in case user imports it
rawConfig.externals = [
...(Array.isArray(rawConfig.externals) ? rawConfig.externals : [rawConfig.externals]),
{
...(inlineVue || {
vue: {
commonjs: 'vue',
commonjs2: 'vue',
root: 'Vue'
}
})
}
].filter(Boolean)
rawConfig.entry = {
[entryName]: realEntry
}
rawConfig.output = Object.assign({
library: libName,
libraryExport: isVueEntry ? 'default' : undefined,
libraryTarget: format,
// preserve UDM header from webpack 3 until webpack provides either
// 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, {
filename: `${entryName}.js`,
chunkFilename: `${entryName}.[name].js`,
// use dynamic publicPath so this can be deployed anywhere
// the actual path will be determined at runtime by checking
// document.currentScript.src.
publicPath: ''
})
if (format === 'commonjs2') {
// #6188
delete rawConfig.output.library
}
return rawConfig
}
const configMap = {
commonjs: genConfig('commonjs2', 'common'),
umd: genConfig('umd', undefined, true),
'umd-min': genConfig('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
}

View File

@ -0,0 +1,143 @@
const path = require('path')
const { resolveEntry, fileToComponentName } = require('./resolveWcEntry')
module.exports = (api, { target, entry, name, 'inline-vue': inlineVue }) => {
// Disable CSS extraction and turn on CSS shadow mode for vue-style-loader
process.env.VUE_CLI_CSS_SHADOW_MODE = true
const { log, error } = require('@vue/cli-shared-utils')
const abort = msg => {
log()
error(msg)
process.exit(1)
}
const cwd = api.getCwd()
const webpack = require('webpack')
const vueMajor = require('../../util/getVueMajor')(cwd)
if (vueMajor === 3) {
abort(`Vue 3 support of the web component target is still under development.`)
}
const isAsync = /async/.test(target)
// generate dynamic entry based on glob files
const resolvedFiles = require('globby').sync(entry.split(','), { cwd: api.resolve('.') })
if (!resolvedFiles.length) {
abort(`entry pattern "${entry}" did not match any files.`)
}
let libName
let prefix
if (resolvedFiles.length === 1) {
// in single mode, determine the lib name from filename
libName = name || fileToComponentName('', resolvedFiles[0]).kebabName
prefix = ''
if (libName.indexOf('-') < 0) {
abort(`--name must contain a hyphen when building a single web component.`)
}
} else {
// multi mode
libName = prefix = (name || api.service.pkg.name)
if (!libName) {
abort(`--name is required when building multiple web components.`)
}
}
const dynamicEntry = resolveEntry(prefix, libName, resolvedFiles, isAsync)
function genConfig (minify, genHTML) {
const config = api.resolveChainableWebpackConfig()
// make sure not to transpile wc-wrapper
config.module
.rule('js')
.exclude
.add(/vue-wc-wrapper/)
// only minify min entry
if (!minify) {
config.optimization.minimize(false)
}
config
.plugin('webpack-virtual-modules')
.use(require('webpack-virtual-modules'), [{
[dynamicEntry.filePath]: dynamicEntry.content
}])
config
.plugin('web-component-options')
.use(webpack.DefinePlugin, [{
'process.env.CUSTOM_ELEMENT_NAME': JSON.stringify(libName)
}])
// enable shadow mode in vue-loader
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.shadowMode = true
return options
})
if (genHTML) {
config
.plugin('demo-html')
.use(require('html-webpack-plugin'), [{
template: path.resolve(__dirname, `./demo-wc.html`),
inject: false,
filename: 'demo.html',
libName,
vueMajor,
components:
prefix === ''
? [libName]
: resolvedFiles.map(file => {
return fileToComponentName(prefix, file).kebabName
})
}])
}
// set entry/output last so it takes higher priority than user
// configureWebpack hooks
// set proxy entry for *.vue files
config.resolve
.alias
.set('~root', api.resolve('.'))
const rawConfig = api.resolveWebpackConfig(config)
// externalize Vue in case user imports it
rawConfig.externals = [
...(Array.isArray(rawConfig.externals) ? rawConfig.externals : [rawConfig.externals]),
{ ...(inlineVue || { vue: 'Vue' }) }
].filter(Boolean)
const entryName = `${libName}${minify ? `.min` : ``}`
rawConfig.entry = {
[entryName]: dynamicEntry.filePath
}
Object.assign(rawConfig.output, {
filename: `${entryName}.js`,
chunkFilename: `${libName}.[name]${minify ? `.min` : ``}.js`,
// use dynamic publicPath so this can be deployed anywhere
// the actual path will be determined at runtime by checking
// document.currentScript.src.
publicPath: ''
})
// to ensure that multiple copies of async wc bundles can co-exist
// on the same page.
rawConfig.output.uniqueName = `vue-lib-${libName}`
return rawConfig
}
return [
genConfig(false, true),
genConfig(true, false)
]
}

View File

@ -0,0 +1,67 @@
const path = require('path')
const camelizeRE = /-(\w)/g
const camelize = str => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}
const hyphenateRE = /\B([A-Z])/g
const hyphenate = str => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
}
/**
* Creates the script to add the component to the custom elements
* @param {string} prefix The prefix for the component library
* @param {string} component The component name for single entry builds, component file for multi-entry builds
* @param {string} file The file for the component
* @param {boolean} isAsync Whether to load component async or not
*/
const createElement = (prefix, component, file, isAsync) => {
const { camelName, kebabName } = exports.fileToComponentName(prefix, component)
return isAsync
? `window.customElements.define('${kebabName}', wrap(Vue, () => import('~root/${file}?shadow')))\n`
: `import ${camelName} from '~root/${file}?shadow'\n` +
`window.customElements.define('${kebabName}', wrap(Vue, ${camelName}))\n`
}
exports.fileToComponentName = (prefix, file) => {
const basename = path.basename(file).replace(/\.(jsx?|vue)$/, '')
const camelName = camelize(basename)
const kebabName = `${prefix ? `${prefix}-` : ``}${hyphenate(basename)}`
return {
basename,
camelName,
kebabName
}
}
exports.resolveEntry = (prefix, libName, files, isAsync) => {
const filePath = path.resolve(__dirname, 'entry-wc.js')
const elements =
prefix === ''
? [createElement('', libName, files[0])]
: files.map(file => createElement(prefix, file, file, isAsync)).join('\n')
function resolveImportPath (mod) {
return require.resolve(mod).replace(/\\/g, '\\\\')
}
const content = `
import './setPublicPath'
import Vue from 'vue'
import wrap from '@vue/web-component-wrapper'
// runtime shared by every component chunk
import '${resolveImportPath('css-loader/dist/runtime/api.js')}'
import '${resolveImportPath('vue-style-loader/lib/addStylesShadow')}'
import '${resolveImportPath('@vue/vue-loader-v15/lib/runtime/componentNormalizer')}'
${elements}`.trim()
return {
filePath: filePath,
content: content
}
}

View File

@ -0,0 +1,23 @@
/* eslint-disable no-var */
// This file is imported into lib/wc client bundles.
if (typeof window !== 'undefined') {
var currentScript = window.document.currentScript
if (process.env.NEED_CURRENTSCRIPT_POLYFILL) {
var getCurrentScript = require('@soda/get-current-script')
currentScript = getCurrentScript()
// for backward compatibility, because previously we directly included the polyfill
if (!('currentScript' in document)) {
Object.defineProperty(document, 'currentScript', { get: getCurrentScript })
}
}
var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/)
if (src) {
__webpack_public_path__ = src[1] // eslint-disable-line
}
}
// Indicate to webpack that this file can be concatenated
export default null