vivo-hap-toolkit
Version:
A command line toolkit for developing Quick Apps.
544 lines (507 loc) • 17.2 kB
JavaScript
/*
* Copyright (C) 2017, hapjs.org. All rights reserved.
*/
const path = require('path')
const fs = require('fs')
const builtinList = require('module').builtinModules
const fsExtra = require('fs-extra')
const webpack = require('webpack')
const { colorconsole, KnownError, getProjectDslName } = require('@vivo-hap-toolkit/shared-utils')
const globalConfig = require('@vivo-hap-toolkit/shared-utils/config')
const { ENTRY_TYPE, loaderWrapper } = require('@vivo-hap-toolkit/packager/lib/common/utils')
const { name, resolveFile } = require('@vivo-hap-toolkit/packager/lib/common/info')
const packagerPostPath = require.resolve('@vivo-hap-toolkit/packager/lib/webpack.post.js')
const xvmPostPath = require.resolve(`@vivo-hap-toolkit/dsl-xvm/lib/webpack.post.js`)
const vuePostPath = require.resolve(`@vivo-hap-toolkit/dsl-vue/lib/webpack.post.js`)
const pathMap = {
packager: packagerPostPath,
xvm: xvmPostPath,
vue: vuePostPath
}
// 主包保留名
const MAIN_PKG_NAME = 'base'
// 能使用rpks能力的调试器最低版本
const RPKS_SUPPORT_VERSION_FROM = 1040
/**
* 获取json文件
* @param pathJson
*/
function getJson(pathJson) {
let config
if (fs.existsSync(pathJson)) {
config = JSON.parse(fs.readFileSync(pathJson))
}
return config || {}
}
/**
* 提取其中的应用,页面,worker的脚本文件
* @return {Array}
* 以 basedir 为基本目录,获取 manifest 的配置的入口页面
*
* @param {ManifestObject} manifest - manifest
* @param {String} basedir - 扫描目录
* @param {String} cwd - 工作目录
* @returns {Array<Object>}
*/
function resolveEntries(manifest, basedir, cwd) {
if (!manifest.router) {
throw Error('manifest.json 中未配置路由!')
}
const entries = {}
const pagesConf = manifest.router.pages || {}
const widgetsConf = manifest.router.widgets || {}
const confsList = [
{
confs: widgetsConf,
type: ENTRY_TYPE.CARD
}
]
if (!process.env.BUILD_CARD_ONLY) {
confsList.unshift({
confs: pagesConf,
type: ENTRY_TYPE.PAGE
})
}
const appFile = resolveFile(path.join(basedir, 'app'))
if (!appFile) {
colorconsole.error('app 文件不存在')
process.exit(1)
}
entries['app'] = './' + path.relative(cwd, appFile) + `?uxType=${ENTRY_TYPE.APP}`
confsList.forEach(({ confs, type }) => {
Object.keys(confs).forEach(routePath => {
const conf = confs[routePath]
const entryKey = path.join(routePath, conf.component)
const filepath = resolveFile(path.join(basedir, entryKey))
if (!filepath) {
colorconsole.throw(`编译失败:请确认manifest.json中配置的文件路径存在:${entryKey}`)
}
let sourceFile = path.relative(cwd, filepath)
sourceFile = './' + sourceFile + `?uxType=${type}`
sourceFile = sourceFile.replace(/\\/g, '/')
entries[entryKey] = sourceFile
})
})
const workers = manifest.workers
if (workers && workers.entries && workers.entries instanceof Array) {
workers.entries
.filter(worker => worker.file)
.forEach(worker => {
entries[worker.file.replace(/\.js$/, '')] = './src/' + worker.file
})
}
return entries
}
/**
* 动态生成 webpack 配置项
*
* @param {Object} options - 命令行参数对象
* @param {String} [options.cwd] - 工作目录
* @param {String} [options.devtool=undefined] - devtool(sourcemap)配置
* @param {boolean} [options.debug=false] - 是否开启调试
* @param {boolean} [options.stats=false] - 是否开启分析
* @param {boolean} [options.disableSubpackages=false] - 是否禁止分包
* @param {boolean} [options.optimizeCssAttr=false] - 优化 css 属性
* @param {boolean} [options.optimizeDescMeta=false] - 优化 css 描述数据
* @param {boolean} [options.optimizeTemplateAttr=false] - 优化模板属性
* @param {boolean} [options.optimizeStyleAppLevel=false] - 优化 app 样式等级
* @param {boolean} [options.optimizeStylePageLevel=false] - 优化 app 样式等级
* @param {production|development} mode - webpack mode
* @returns {WebpackConfiguration}
*/
module.exports = function genWebpackConf(options, mode) {
// 项目目录
if (options.cwd) {
globalConfig.projectPath = options.cwd
}
const cwd = globalConfig.projectPath
// 支持文件扩展名
const FILE_EXT_LIST = name.extList
// 源代码目录
const SRC_DIR = path.join(cwd, globalConfig.sourceRoot)
// 签名文件目录
const SIGN_FOLDER = globalConfig.signRoot
// 编译文件的目录
const BUILD_DIR = path.join(cwd, globalConfig.outputPath)
// 最终发布目录
const DIST_DIR = path.join(cwd, globalConfig.releasePath)
// 打包配置文件
const manifestFile = path.join(SRC_DIR, 'manifest.json')
const pathPackageJson = path.join(cwd, 'package.json')
const packageJson = getJson(pathPackageJson)
// 校验项目工程
validateProject()
// 清理 BUILD_DIR DIST_DIR
cleanup()
let manifest
try {
manifest = getJson(manifestFile)
} catch (e) {
throw new KnownError('manifest.json 解析失败!')
}
validateManifest(manifest, options)
// 设置合适的v8版本
setAdaptForV8Version(options.disableScriptV8V65)
// 页面文件
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,
// 是否注入测试框架
NODE_TEST: process.env.NODE_TEST
}
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'
}),
// 编译耗时
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()
})
}
],
node: {
global: false
},
resolve: {
modules: ['node_modules'],
extensions: ['.webpack.js', '.web.js', '.js', '.json'].concat(FILE_EXT_LIST)
},
stats: {
builtAt: false,
entrypoints: false,
children: false,
chunks: false,
chunkModules: false,
chunkOrigins: false,
modules: false,
version: false,
assets: false
}
}
// 加载配置
loadWebpackConfList()
// 设置 sourcemap 类型
webpackConf.devtool = getDevtoolValue(webpackConf.mode, options.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, 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,
versionCode,
nodeConf: env,
pathDist: DIST_DIR,
pathSrc: SRC_DIR,
subpackages,
pathBuild: BUILD_DIR,
pathSignFolder: SIGN_FOLDER,
workers,
cwd
},
{
...options,
loaderWrapper: loaderWrapper.bind(null, SRC_DIR)
}
)
}
} catch (err) {
console.error(`加载webpack配置文件[${fileConf}]出错:${err.message}`, err)
}
}
}
}
/**
* 验证项目配置正确
*/
function validateProject() {
if (!fs.existsSync(manifestFile)) {
colorconsole.throw(
`请确认项目%projectDir%/${globalConfig.sourceRoot}/下存在manifest.json文件:${manifestFile}`
)
throw new KnownError(`找不到 ${globalConfig.sourceRoot}/manifest.json`)
}
}
/**
* 清理 BUILD_DIR DIST_DIR
*/
function cleanup() {
fsExtra.emptyDirSync(BUILD_DIR)
// 清空 dist 目录下的文件(仅文件)
if (fs.existsSync(DIST_DIR)) {
const zipfiles = fs.readdirSync(DIST_DIR)
zipfiles.forEach(function(file) {
const curPath = DIST_DIR + '/' + file
if (fs.statSync(curPath).isFile()) {
fs.unlinkSync(curPath)
}
})
}
}
/**
* 设置v8版本
* @param {boolean} disableScriptV8V65
*/
function setAdaptForV8Version(disableScriptV8V65) {
const minPlatformVersion = parseInt(manifest.minPlatformVersion)
if (fs.existsSync(pathPackageJson)) {
if (!disableScriptV8V65 && minPlatformVersion >= 1040) {
const hasDefinedChrome65 =
packageJson.browserslist && packageJson.browserslist.includes('chrome 65')
colorconsole.log(
`当前minPlatformVersion >= 1040,平台采用v8版本为6.5+(对应chrome版本为65版+),工具将不再对V8 6.5版本支持的ES6代码进行转换`
)
if (hasDefinedChrome65) return
// v8 6.5相当于chrome 65版本
packageJson.browserslist = ['chrome 65']
fs.writeFileSync(pathPackageJson, JSON.stringify(packageJson, null, 2))
} else if (packageJson.browserslist) {
delete packageJson.browserslist
fs.writeFileSync(pathPackageJson, JSON.stringify(packageJson, null, 2))
}
}
}
/**
* 验证项目的应用全局配置
* @param {Manifest} manifest - manifest 对象
*/
function validateManifest(manifest, options) {
const { subpackages } = manifest
// 验证分包规则
if (!options.disableSubpackages && subpackages && subpackages.length > 0) {
validateManifestSubpackages(subpackages)
}
}
/**
* 检查subpackages字段配置。
* 除subpackages字段指定的文件是打进非主包外,剩余文件都打进主包
* 主包与是独立包的非主包,都需要manifest文件
* @param {object[]} subpackages 分包列表: [{ name, resource, standalone }]
* @param {string} subpackages[].name 分包名字,必填,不能重复,且不能是"base"(这是主包保留名),只能是 数字字母_ 组成
* @param {string} subpackages[].resource 分包资源路径,必须为src下文件目录,不能重复,分包间不能有包含关系,只能是 数字字母_ 开头,数字字母_-/ 组成
* @param {boolean} subpackages[].standalone 是否独立包标识,是独立包则需要manifest文件,缺省为false;
*/
function validateManifestSubpackages(subpackages) {
// 分包名的校验规则
const nameReg = /^\w+$/
// 资源名的校验规则
const resourceReg = /^\w[\w-/]*$/
// 用以检测分包名是否重复
const nameList = []
// 用以检测分包资源路径是否重复
const resList = []
let name = ''
let resource = ''
// 资源路径的具体文件路径
let resPath = ''
let index = 0
/**
* 检查当前资源路径与已校验过的资源路径是否有包含关系。
*
* @param {string} resource - 当前要校验的资源
* @param {number} index - 当前要校验资源的序号
* @return {boolean} true/false - 存在/不存在
*/
function checkPathInclusion(resource, currentIndex) {
for (let i = 0, l = resList.length; i < l; i++) {
const _res = resList[i]
if (resource.startsWith(_res) || _res.startsWith(resource)) {
colorconsole.throw(
`第${currentIndex}分包的资源'${resource}'与第${i +
1}分包的资源'${_res}'有包含关系,请修改`
)
return true
}
}
return false
}
subpackages.forEach((subpkg, i) => {
name = subpkg.name
resource = subpkg.resource
resPath = resource && path.join(SRC_DIR, resource)
index = i + 1
if (!name) {
colorconsole.throw(`第${index}分包的名字不能为空,请添加`)
} else if (!nameReg.test(name)) {
colorconsole.throw(`第${index}分包的名字'${name}'不合法,只能是数字字母下划线组成,请修改`)
} else if (name === MAIN_PKG_NAME) {
colorconsole.throw(`第${index}分包的名字'${name}'是主包保留名,请修改`)
} else if (nameList.indexOf(name) > -1) {
colorconsole.throw(`第${index}分包的名字'${name}'已存在,请修改`)
} else {
nameList.push(name)
}
if (!resource) {
colorconsole.throw(`第${index}分包的资源名不能为空,请添加`)
} else if (!resourceReg.test(resource)) {
colorconsole.throw(
`第${index}分包的资源名'${resource}'不合法,只能是 数字字母_ 开头,数字字母_-/ 组成,请修改`
)
} else if (resList.indexOf(resource) > -1) {
colorconsole.throw(`第${index}分包的资源'${resource}'已被使用,请修改`)
} else if (!fs.existsSync(resPath)) {
colorconsole.throw(`第${index}分包的资源'${resource}', 文件目录'${resPath}'不存在,请修改`)
} else if (!checkPathInclusion(resource, index)) {
resList.push(resource)
}
})
colorconsole.warn(
`项目已配置分包,若想使用分包功能,请确保平台版本 >= ${RPKS_SUPPORT_VERSION_FROM}`
)
}
/**
* 使用 node 原生模块给予警告
*/
function checkBuiltinModules(context, request, callback) {
// 提取 package.json 中的依赖
let projectDependencies = []
if (packageJson.devDependencies) {
projectDependencies = Object.keys(packageJson.devDependencies)
}
if (packageJson.dependencies) {
projectDependencies = projectDependencies.concat(Object.keys(packageJson.dependencies))
}
// 枚举 node 原生模块
const enumList = [
'assert',
'console',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'sys',
'timers',
'tls',
'tty',
'url',
'util',
'vm',
'zlib'
]
const externalsList = Array.isArray(builtinList) ? builtinList : enumList
// 确定是node原生模块,并且没有在package.json 中引用这个模块
if (externalsList.indexOf(request) > -1 && projectDependencies.indexOf(request) === -1) {
colorconsole.warn(
`您当前使用了 ${request} 似乎是 node 原生模块, 快应用不是 node 环境不支持 node 原生模块`
)
}
callback()
}
/**
* 校验并返回webpack devtool值(sourcemap)
* @param {String} mode - webpack mode
* @param {String} devtool - devtool参数值
*/
function getDevtoolValue(mode, devtool) {
const sourcemaps = {
development: {
default: 'cheap-module-eval-source-map',
options: [
'none',
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
},
production: {
default: 'none',
options: [
'none',
'cheap-source-map',
'cheap-module-source-map',
'source-map',
'hidden-source-map',
'nosources-source-map'
]
}
}
const sourcemapArr = sourcemaps[mode].options
const defaultSourcemap = sourcemaps[mode].default
if (typeof devtool !== 'string') {
return defaultSourcemap
}
if (sourcemapArr.indexOf(devtool) === -1) {
colorconsole.warn(`${mode} 模式 devtool 不支持 '${devtool}', 改为默认 '${defaultSourcemap}'`)
return defaultSourcemap
}
return devtool
}
return webpackConf
}