vivo-hap-toolkit
Version:
A command line toolkit for developing Quick Apps.
370 lines (338 loc) • 11.8 kB
JavaScript
/*
* Copyright (C) 2017, hapjs.org. All rights reserved.
*/
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const {
readJson,
colorconsole,
KnownError,
getProjectDslName,
getDefaultServerHost
} = require('@vivo-hap-toolkit/shared-utils')
const globalConfig = require('@vivo-hap-toolkit/shared-utils/config')
const {
compileOptionsMeta,
compileOptionsObject,
initCompileOptionsObject
} = require('@vivo-hap-toolkit/shared-utils/compilation-config')
const { name } = require('@vivo-hap-toolkit/packager/lib/common/info')
const ManifestWatchPlugin = require('../lib/plugins/manifest-watch-plugin')
const { resolveEntries } = require('../lib/utils')
const getDevtool = require('./get-devtool')
const {
getConfigPath,
cleanup,
checkBuiltinModules,
setAdaptForV8Version,
checkBabelModulesExists
} = require('./helpers')
const { validateProject, validateManifest, valiedateSitemap } = require('./validate')
const pathMap = {
packager: require.resolve('@vivo-hap-toolkit/packager/lib/webpack.post.js'),
xvm: require.resolve(`@vivo-hap-toolkit/dsl-xvm/lib/webpack.post.js`),
vue: require.resolve(`@vivo-hap-toolkit/dsl-vue/lib/webpack.post.js`)
}
// 能使用抽取公共js功能的调试器最低版本
const SPLIT_CHUNKS_SUPPORT_VERSION_FROM = 1080
/**
* 动态生成 webpack 配置项
*
* @param {Object} launchOptions - 命令行参数对象
* @param {String} [launchOptions.cwd] - 工作目录
* @param {String} [launchOptions.originType] - 打包来源,ide|cmd
* @param {String} [launchOptions.devtool=undefined] - devtool(sourcemap)配置
* @param {boolean} [launchOptions.debug=false] - 是否开启调试
* @param {boolean} [launchOptions.stats=false] - 是否开启分析
* @param {boolean} [launchOptions.disableSubpackages=false] - 是否禁止分包
* @param {boolean} [launchOptions.optimizeCssAttr=false] - 优化 css 属性
* @param {boolean} [launchOptions.optimizeDescMeta=false] - 优化 css 描述数据
* @param {boolean} [launchOptions.optimizeTemplateAttr=false] - 优化模板属性
* @param {boolean} [launchOptions.optimizeStyleAppLevel=false] - 优化 app 样式等级
* @param {boolean} [launchOptions.optimizeStylePageLevel=false] - 优化 app 样式等级
* @param {boolean} [launchOptions.splitChunksMode=undefined] - 抽取公共JS
* @param {production|development} mode - webpack mode
* @returns {WebpackConfiguration}
*/
module.exports = function genWebpackConf(launchOptions, mode) {
// 项目目录
if (launchOptions.cwd) {
globalConfig.projectPath = launchOptions.cwd
}
const cwd = globalConfig.projectPath
const hapConfigPath = getConfigPath(cwd)
// 用于接受quickapp.config.js 或者 hap.config.js中的配置
let quickappConfig
// 接受命令行
let cli = {}
if (hapConfigPath) {
try {
quickappConfig = require(hapConfigPath)
if (typeof quickappConfig.cli === 'object') {
cli = quickappConfig.cli
launchOptions = Object.assign({}, cli, launchOptions)
}
} catch (err) {
colorconsole.error(`加载webpack配置文件[${hapConfigPath}]出错:${err.message}`)
}
}
// 源代码目录
const SRC_DIR = path.resolve(cwd, globalConfig.sourceRoot)
// 签名文件目录
const SIGN_FOLDER = globalConfig.signRoot
// 编译文件的目录
const BUILD_DIR = path.resolve(cwd, globalConfig.outputPath)
// 最终发布目录
const DIST_DIR = path.resolve(cwd, globalConfig.releasePath)
// 打包配置文件
const manifestFile = path.resolve(SRC_DIR, 'manifest.json')
checkBabelModulesExists(cwd)
// 合并launchOptions到全局
initCompileOptionsObject(launchOptions)
// 校验项目工程
validateProject(manifestFile)
// 清理 BUILD_DIR DIST_DIR
cleanup(BUILD_DIR, DIST_DIR)
let manifest
try {
manifest = readJson(manifestFile)
} catch (e) {
throw new KnownError('manifest.json 解析失败!')
}
validateManifest(SRC_DIR, manifest, compileOptionsObject)
valiedateSitemap(SRC_DIR, manifest)
// 设置合适的v8版本
setAdaptForV8Version(compileOptionsObject.disableScriptV8V65, manifest, cwd)
// 页面文件
const entries = resolveEntries(manifest, SRC_DIR, cwd)
// 环境变量
const env = {
// 平台:native
NODE_PLATFORM: process.env.NODE_PLATFORM,
// 阶段: dev|test|release
NODE_PHASE: process.env.NODE_PHASE
}
colorconsole.info(`配置环境:${JSON.stringify(env)}`)
const webpackConf = {
context: cwd,
mode,
entry: entries,
output: {
path: BUILD_DIR,
filename: '[name].js'
},
module: {
rules: []
},
externals: [checkBuiltinModules],
plugins: [
// 定义环境变量
new webpack.DefinePlugin({
// 平台:na
ENV_PLATFORM: JSON.stringify(env.NODE_PLATFORM),
// 阶段: dev|test|release
ENV_PHASE: JSON.stringify(env.NODE_PHASE),
ENV_PHASE_DV: env.NODE_PHASE === 'dev',
ENV_PHASE_QA: env.NODE_PHASE === 'test',
ENV_PHASE_OL: env.NODE_PHASE === 'prod',
// 服务器地址
QUICKAPP_SERVER_HOST: JSON.stringify(getDefaultServerHost()),
QUICKAPP_TOOLKIT_VERSION: JSON.stringify(require('../package.json').version)
}),
// 编译耗时
function BuildTimePlugin() {
this.hooks.done.tapAsync('end', function(stats, callback) {
if (!stats.compilation.errors.length) {
const secs = (stats.endTime - stats.startTime) / 1000
colorconsole.info(`Build Time Cost: ${secs}s`)
}
callback()
})
},
new ManifestWatchPlugin({
appRoot: cwd,
root: SRC_DIR
})
],
node: false,
resolve: {
modules: ['node_modules'],
extensions: ['.webpack.js', '.web.js', '.js', '.json'].concat(name.extList)
},
stats: {
builtAt: false,
entrypoints: false,
children: false,
chunks: false,
chunkModules: false,
chunkOrigins: false,
modules: false,
version: false,
assets: false
},
optimization: {}
}
// 设置抽取公共js
if (compileOptionsObject.splitChunksMode === compileOptionsMeta.splitChunksModeEnum.SMART) {
colorconsole.warn(
`启用splitChunksMode:SMART模式, 请确保平台版本 >= ${SPLIT_CHUNKS_SUPPORT_VERSION_FROM}`
)
// 当前仅smart模式才启用
webpackConf.optimization.splitChunks = {
minSize: 1,
maxSize: 0,
cacheGroups: {
default: false,
vendors: {
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
minChunks: 2,
name(module) {
// 处理node_modules的chunk位置为node_modules下的路径,通过name来控制,如node_modules/vue/dist/vue.runtime.esm;
const index = module.resource.indexOf('node_modules')
const chunkPath = module.resource
.slice(index)
.replace(/(.+)\.\w\??(.+)?/, ($0, $1) => $1)
// 兼容windows上的路径
return chunkPath.split(path.sep).join('/')
},
priority: 2,
reuseExistingChunk: true,
enforce: true
},
// 用于抽离出app.ux引入的组件或者文件
appPkg: {
test(module) {
function isAppRequire(module) {
let result = false
module.chunksIterable.forEach(chunk => {
if (chunk.name === 'app') {
result = true
return false
}
})
return result
}
function isUxPath(module) {
const queryStriped = module.resource.split('?').shift()
return path.extname(queryStriped) === '.ux'
}
if (isAppRequire(module) && isUxPath(module)) {
return true
}
return false
},
chunks: 'all',
name(module) {
// 处理正常chunk的位置为相对src下的路径,通过name来控制,如Common/a;
const sourcePath = path.join(globalConfig.projectPath, globalConfig.sourceRoot)
const chunkPath = module.resource
.slice(sourcePath.length + 1)
.replace(/(.+)\.\w\??(.+)?/, ($0, $1) => $1)
// 兼容windows上的路径
return chunkPath.split(path.sep).join('/')
},
// 优先级最低
priority: 1,
// 只要app.ux引入即被抽离
minChunks: 1,
reuseExistingChunk: true,
enforce: true
},
chunks: {
test(module) {
// 过滤掉自定义module,比如抽取css生成*.css.json使用的CssModule
if (module.constructor.name !== 'NormalModule') {
return false
}
return true
},
chunks: 'all',
minChunks: 2,
name(module) {
// 处理正常chunk的位置为相对src下的路径,通过name来控制,如Common/a;
const sourcePath = path.join(globalConfig.projectPath, globalConfig.sourceRoot)
const chunkPath = module.resource
.slice(sourcePath.length + 1)
.replace(/(.+)\.\w\??(.+)?/, ($0, $1) => $1)
// 兼容windows上的路径
return chunkPath.split(path.sep).join('/')
},
priority: 1,
reuseExistingChunk: true,
enforce: true
},
async: {
chunks: 'async',
name(module) {
const sourcePath = path.join(globalConfig.projectPath, globalConfig.sourceRoot)
const chunkPath = module.resource
.slice(sourcePath.length + 1)
.replace(/(.+)\.\w\??(.+)?/, ($0, $1) => $1)
// 兼容windows上的路径
return chunkPath.split(path.sep).join('/')
},
priority: 3,
enforce: true
}
}
}
}
// 加载配置
loadWebpackConfList()
// 设置 sourcemap 类型
webpackConf.devtool = getDevtool(webpackConf.mode, compileOptionsObject.devtool)
/**
* 尝试加载每个模块的webpack配置
*/
function loadWebpackConfList() {
const moduleList = [
{
name: 'packager',
path: pathMap['packager']
}
]
const dslName = getProjectDslName(cwd)
moduleList.push({
name: `${dslName}-post`,
path: pathMap[dslName]
})
const { package: appPackageName, icon: appIcon, versionCode, subpackages, workers } = manifest
for (let i = 0, len = moduleList.length; i < len; i++) {
const fileConf = moduleList[i].path
if (fs.existsSync(fileConf)) {
try {
const moduleWebpackConf = require(fileConf)
if (moduleWebpackConf.postHook) {
moduleWebpackConf.postHook(
webpackConf,
{
appPackageName,
appIcon,
versionCode,
nodeConf: env,
pathDist: DIST_DIR,
pathSrc: SRC_DIR,
subpackages,
pathBuild: BUILD_DIR,
pathSignFolder: SIGN_FOLDER,
workers,
cwd,
originType: compileOptionsObject.originType
},
quickappConfig
)
}
} catch (err) {
console.error(`加载 webpack 配置文件[${fileConf}]出错:${err.message}`, err)
}
}
}
// 增加项目目录的postHook机制
if (quickappConfig && quickappConfig.postHook) {
quickappConfig.postHook(webpackConf, compileOptionsObject)
}
}
return webpackConf
}