UNPKG

17.1 kBJavaScriptView Raw
1/*
2 * Copyright (C) 2017, hapjs.org. All rights reserved.
3 */
4
5const path = require('path')
6const fs = require('fs')
7const builtinList = require('module').builtinModules
8const fsExtra = require('fs-extra')
9const webpack = require('webpack')
10const { colorconsole, KnownError, getProjectDslName } = require('@hap-toolkit/shared-utils')
11const globalConfig = require('@hap-toolkit/shared-utils/config')
12const { ENTRY_TYPE, loaderWrapper } = require('@hap-toolkit/packager/lib/common/utils')
13const { name, resolveFile } = require('@hap-toolkit/packager/lib/common/info')
14
15const packagerPostPath = require.resolve('@hap-toolkit/packager/lib/webpack.post.js')
16const xvmPostPath = require.resolve(`@hap-toolkit/dsl-xvm/lib/webpack.post.js`)
17const vuePostPath = require.resolve(`@hap-toolkit/dsl-vue/lib/webpack.post.js`)
18
19const pathMap = {
20 packager: packagerPostPath,
21 xvm: xvmPostPath,
22 vue: vuePostPath
23}
24
25// 主包保留名
26const MAIN_PKG_NAME = 'base'
27// 能使用rpks能力的调试器最低版本
28const RPKS_SUPPORT_VERSION_FROM = 1040
29
30/**
31 * 获取json文件
32 * @param pathJson
33 */
34function getJson(pathJson) {
35 let config
36 if (fs.existsSync(pathJson)) {
37 config = JSON.parse(fs.readFileSync(pathJson))
38 }
39 return config || {}
40}
41
42/**
43 * 提取其中的应用,页面,worker的脚本文件
44 * @return {Array}
45 * 以 basedir 为基本目录,获取 manifest 的配置的入口页面
46 *
47 * @param {ManifestObject} manifest - manifest
48 * @param {String} basedir - 扫描目录
49 * @param {String} cwd - 工作目录
50 * @returns {Array<Object>}
51 */
52function resolveEntries(manifest, basedir, cwd) {
53 if (!manifest.router) {
54 throw Error('manifest.json 中未配置路由!')
55 }
56 const entries = {}
57 const pagesConf = manifest.router.pages || {}
58 const widgetsConf = manifest.router.widgets || {}
59 const confsList = [
60 {
61 confs: widgetsConf,
62 type: ENTRY_TYPE.CARD
63 }
64 ]
65 confsList.unshift({
66 confs: pagesConf,
67 type: ENTRY_TYPE.PAGE
68 })
69 const appFile = resolveFile(path.join(basedir, 'app'))
70 if (!appFile) {
71 colorconsole.error('app 文件不存在')
72 process.exit(1)
73 }
74 entries['app'] = './' + path.relative(cwd, appFile) + `?uxType=${ENTRY_TYPE.APP}`
75 confsList.forEach(({ confs, type }) => {
76 Object.keys(confs).forEach(routePath => {
77 const conf = confs[routePath]
78 const entryKey = path.join(routePath, conf.component)
79 const filepath = resolveFile(path.join(basedir, entryKey))
80
81 if (!filepath) {
82 colorconsole.throw(`编译失败:请确认manifest.json中配置的文件路径存在:${entryKey}`)
83 }
84
85 let sourceFile = path.relative(cwd, filepath)
86 sourceFile = './' + sourceFile + `?uxType=${type}`
87 sourceFile = sourceFile.replace(/\\/g, '/')
88 entries[entryKey] = sourceFile
89 })
90 })
91 const workers = manifest.workers
92 if (workers && workers.entries && workers.entries instanceof Array) {
93 workers.entries
94 .filter(worker => worker.file)
95 .forEach(worker => {
96 entries[worker.file.replace(/\.js$/, '')] = './src/' + worker.file
97 })
98 }
99 return entries
100}
101
102/**
103 * 动态生成 webpack 配置项
104 *
105 * @param {Object} options - 命令行参数对象
106 * @param {String} [options.cwd] - 工作目录
107 * @param {String} [options.devtool=undefined] - devtool(sourcemap)配置
108 * @param {boolean} [options.debug=false] - 是否开启调试
109 * @param {boolean} [options.stats=false] - 是否开启分析
110 * @param {boolean} [options.disableSubpackages=false] - 是否禁止分包
111 * @param {boolean} [options.optimizeCssAttr=false] - 优化 css 属性
112 * @param {boolean} [options.optimizeDescMeta=false] - 优化 css 描述数据
113 * @param {boolean} [options.optimizeTemplateAttr=false] - 优化模板属性
114 * @param {boolean} [options.optimizeStyleAppLevel=false] - 优化 app 样式等级
115 * @param {boolean} [options.optimizeStylePageLevel=false] - 优化 app 样式等级
116 * @param {production|development} mode - webpack mode
117 * @returns {WebpackConfiguration}
118 */
119module.exports = function genWebpackConf(options, mode) {
120 // 项目目录
121 if (options.cwd) {
122 globalConfig.projectPath = options.cwd
123 }
124 const cwd = globalConfig.projectPath
125 // 支持文件扩展名
126 const FILE_EXT_LIST = name.extList
127
128 // 源代码目录
129 const SRC_DIR = path.join(cwd, globalConfig.sourceRoot)
130 // 签名文件目录
131 const SIGN_FOLDER = globalConfig.signRoot
132 // 编译文件的目录
133 const BUILD_DIR = path.join(cwd, globalConfig.outputPath)
134 // 最终发布目录
135 const DIST_DIR = path.join(cwd, globalConfig.releasePath)
136 // 打包配置文件
137 const manifestFile = path.join(SRC_DIR, 'manifest.json')
138
139 const pathPackageJson = path.join(cwd, 'package.json')
140 const packageJson = getJson(pathPackageJson)
141
142 // 校验项目工程
143 validateProject()
144
145 // 清理 BUILD_DIR DIST_DIR
146 cleanup()
147
148 let manifest
149 try {
150 manifest = getJson(manifestFile)
151 } catch (e) {
152 throw new KnownError('manifest.json 解析失败!')
153 }
154 validateManifest(manifest, options)
155
156 // 设置合适的v8版本
157 setAdaptForV8Version(options.disableScriptV8V65)
158
159 // 页面文件
160 const entries = resolveEntries(manifest, SRC_DIR, cwd)
161
162 // 环境变量
163 const env = {
164 // 平台:native
165 NODE_PLATFORM: process.env.NODE_PLATFORM,
166 // 阶段: dev|test|release
167 NODE_PHASE: process.env.NODE_PHASE,
168 // 是否注入测试框架
169 NODE_TEST: process.env.NODE_TEST
170 }
171 colorconsole.info(`配置环境:${JSON.stringify(env)}`)
172
173 const webpackConf = {
174 context: cwd,
175 mode,
176 entry: entries,
177 output: {
178 path: BUILD_DIR,
179 filename: '[name].js'
180 },
181 module: {
182 rules: []
183 },
184 externals: [checkBuiltinModules],
185 plugins: [
186 // 定义环境变量
187 new webpack.DefinePlugin({
188 // 平台:na
189 ENV_PLATFORM: JSON.stringify(env.NODE_PLATFORM),
190 // 阶段: dev|test|release
191 ENV_PHASE: JSON.stringify(env.NODE_PHASE),
192 ENV_PHASE_DV: env.NODE_PHASE === 'dev',
193 ENV_PHASE_QA: env.NODE_PHASE === 'test',
194 ENV_PHASE_OL: env.NODE_PHASE === 'prod'
195 }),
196 // 编译耗时
197 function BuildTimePlugin() {
198 this.hooks.done.tapAsync('end', function(stats, callback) {
199 if (!stats.compilation.errors.length) {
200 const secs = (stats.endTime - stats.startTime) / 1000
201 colorconsole.info(`Build Time Cost: ${secs}s`)
202 }
203 callback()
204 })
205 }
206 ],
207 node: {
208 global: false
209 },
210 resolve: {
211 modules: ['node_modules'],
212 extensions: ['.webpack.js', '.web.js', '.js', '.json'].concat(FILE_EXT_LIST)
213 },
214 stats: {
215 builtAt: false,
216 entrypoints: false,
217 children: false,
218 chunks: false,
219 chunkModules: false,
220 chunkOrigins: false,
221 modules: false,
222 version: false,
223 assets: false
224 }
225 }
226
227 // 加载配置
228 loadWebpackConfList()
229
230 // 设置 sourcemap 类型
231 webpackConf.devtool = getDevtoolValue(webpackConf.mode, options.devtool)
232
233 /**
234 * 尝试加载每个模块的webpack配置
235 */
236 function loadWebpackConfList() {
237 const moduleList = [
238 {
239 name: 'packager',
240 path: pathMap['packager']
241 }
242 ]
243
244 const dslName = getProjectDslName(cwd)
245
246 moduleList.push({
247 name: `${dslName}-post`,
248 path: pathMap[dslName]
249 })
250
251 const { package: appPackageName, versionCode, subpackages, workers } = manifest
252 for (let i = 0, len = moduleList.length; i < len; i++) {
253 const fileConf = moduleList[i].path
254 if (fs.existsSync(fileConf)) {
255 try {
256 const moduleWebpackConf = require(fileConf)
257 if (moduleWebpackConf.postHook) {
258 moduleWebpackConf.postHook(
259 webpackConf,
260 {
261 appPackageName,
262 versionCode,
263 nodeConf: env,
264 pathDist: DIST_DIR,
265 pathSrc: SRC_DIR,
266 subpackages,
267 pathBuild: BUILD_DIR,
268 pathSignFolder: SIGN_FOLDER,
269 workers,
270 cwd
271 },
272 {
273 ...options,
274 loaderWrapper: loaderWrapper.bind(null, SRC_DIR)
275 }
276 )
277 }
278 } catch (err) {
279 console.error(`加载webpack配置文件[${fileConf}]出错:${err.message}`, err)
280 }
281 }
282 }
283 }
284
285 /**
286 * 验证项目配置正确
287 */
288 function validateProject() {
289 if (!fs.existsSync(manifestFile)) {
290 colorconsole.throw(
291 `请确认项目%projectDir%/${globalConfig.sourceRoot}/下存在manifest.json文件:${manifestFile}`
292 )
293 throw new KnownError(`找不到 ${globalConfig.sourceRoot}/manifest.json`)
294 }
295 }
296
297 /**
298 * 清理 BUILD_DIR DIST_DIR
299 */
300 function cleanup() {
301 fsExtra.emptyDirSync(BUILD_DIR)
302
303 // 清空 dist 目录下的文件(仅文件)
304 if (fs.existsSync(DIST_DIR)) {
305 const zipfiles = fs.readdirSync(DIST_DIR)
306 zipfiles.forEach(function(file) {
307 const curPath = DIST_DIR + '/' + file
308 if (fs.statSync(curPath).isFile()) {
309 fs.unlinkSync(curPath)
310 }
311 })
312 }
313 }
314
315 /**
316 * 设置v8版本
317 * @param {boolean} disableScriptV8V65
318 */
319 function setAdaptForV8Version(disableScriptV8V65) {
320 const minPlatformVersion = parseInt(manifest.minPlatformVersion)
321 if (fs.existsSync(pathPackageJson)) {
322 if (!disableScriptV8V65 && minPlatformVersion >= 1040) {
323 const hasDefinedChrome65 =
324 packageJson.browserslist && packageJson.browserslist.includes('chrome 65')
325 colorconsole.log(
326 `当前minPlatformVersion >= 1040,平台采用v8版本为6.5+(对应chrome版本为65版+),工具将不再对V8 6.5版本支持的ES6代码进行转换`
327 )
328 if (hasDefinedChrome65) return
329 // v8 6.5相当于chrome 65版本
330 packageJson.browserslist = ['chrome 65']
331 fs.writeFileSync(pathPackageJson, JSON.stringify(packageJson, null, 2))
332 } else if (packageJson.browserslist) {
333 delete packageJson.browserslist
334 fs.writeFileSync(pathPackageJson, JSON.stringify(packageJson, null, 2))
335 }
336 }
337 }
338
339 /**
340 * 验证项目的应用全局配置
341 * @param {Manifest} manifest - manifest 对象
342 */
343 function validateManifest(manifest, options) {
344 const { subpackages } = manifest
345 // 验证分包规则
346 if (!options.disableSubpackages && subpackages && subpackages.length > 0) {
347 validateManifestSubpackages(subpackages)
348 }
349 }
350
351 /**
352 * 检查subpackages字段配置。
353 * 除subpackages字段指定的文件是打进非主包外,剩余文件都打进主包
354 * 主包与是独立包的非主包,都需要manifest文件
355 * @param {object[]} subpackages 分包列表: [{ name, resource, standalone }]
356 * @param {string} subpackages[].name 分包名字,必填,不能重复,且不能是"base"(这是主包保留名),只能是 数字字母_ 组成
357 * @param {string} subpackages[].resource 分包资源路径,必须为src下文件目录,不能重复,分包间不能有包含关系,只能是 数字字母_ 开头,数字字母_-/ 组成
358 * @param {boolean} subpackages[].standalone 是否独立包标识,是独立包则需要manifest文件,缺省为false;
359 */
360 function validateManifestSubpackages(subpackages) {
361 // 分包名的校验规则
362 const nameReg = /^\w+$/
363 // 资源名的校验规则
364 const resourceReg = /^\w[\w-/]*$/
365 // 用以检测分包名是否重复
366 const nameList = []
367 // 用以检测分包资源路径是否重复
368 const resList = []
369 let name = ''
370 let resource = ''
371
372 // 资源路径的具体文件路径
373 let resPath = ''
374 let index = 0
375
376 /**
377 * 检查当前资源路径与已校验过的资源路径是否有包含关系。
378 *
379 * @param {string} resource - 当前要校验的资源
380 * @param {number} index - 当前要校验资源的序号
381 * @return {boolean} true/false - 存在/不存在
382 */
383 function checkPathInclusion(resource, currentIndex) {
384 for (let i = 0, l = resList.length; i < l; i++) {
385 const _res = resList[i]
386 if (resource.startsWith(_res) || _res.startsWith(resource)) {
387 colorconsole.throw(
388 `第${currentIndex}分包的资源'${resource}'与第${i +
389 1}分包的资源'${_res}'有包含关系,请修改`
390 )
391 return true
392 }
393 }
394 return false
395 }
396
397 subpackages.forEach((subpkg, i) => {
398 name = subpkg.name
399 resource = subpkg.resource
400 resPath = resource && path.join(SRC_DIR, resource)
401 index = i + 1
402 if (!name) {
403 colorconsole.throw(`第${index}分包的名字不能为空,请添加`)
404 } else if (!nameReg.test(name)) {
405 colorconsole.throw(`第${index}分包的名字'${name}'不合法,只能是数字字母下划线组成,请修改`)
406 } else if (name === MAIN_PKG_NAME) {
407 colorconsole.throw(`第${index}分包的名字'${name}'是主包保留名,请修改`)
408 } else if (nameList.indexOf(name) > -1) {
409 colorconsole.throw(`第${index}分包的名字'${name}'已存在,请修改`)
410 } else {
411 nameList.push(name)
412 }
413
414 if (!resource) {
415 colorconsole.throw(`第${index}分包的资源名不能为空,请添加`)
416 } else if (!resourceReg.test(resource)) {
417 colorconsole.throw(
418 `第${index}分包的资源名'${resource}'不合法,只能是 数字字母_ 开头,数字字母_-/ 组成,请修改`
419 )
420 } else if (resList.indexOf(resource) > -1) {
421 colorconsole.throw(`第${index}分包的资源'${resource}'已被使用,请修改`)
422 } else if (!fs.existsSync(resPath)) {
423 colorconsole.throw(`第${index}分包的资源'${resource}', 文件目录'${resPath}'不存在,请修改`)
424 } else if (!checkPathInclusion(resource, index)) {
425 resList.push(resource)
426 }
427 })
428 colorconsole.warn(
429 `项目已配置分包,若想使用分包功能,请确保平台版本 >= ${RPKS_SUPPORT_VERSION_FROM}`
430 )
431 }
432
433 /**
434 * 使用 node 原生模块给予警告
435 */
436 function checkBuiltinModules(context, request, callback) {
437 // 提取 package.json 中的依赖
438 let projectDependencies = []
439 if (packageJson.devDependencies) {
440 projectDependencies = Object.keys(packageJson.devDependencies)
441 }
442 if (packageJson.dependencies) {
443 projectDependencies = projectDependencies.concat(Object.keys(packageJson.dependencies))
444 }
445
446 // 枚举 node 原生模块
447 const enumList = [
448 'assert',
449 'console',
450 'buffer',
451 'child_process',
452 'cluster',
453 'console',
454 'constants',
455 'crypto',
456 'dgram',
457 'dns',
458 'domain',
459 'events',
460 'fs',
461 'http',
462 'https',
463 'module',
464 'net',
465 'os',
466 'path',
467 'process',
468 'punycode',
469 'querystring',
470 'readline',
471 'repl',
472 'stream',
473 'string_decoder',
474 'sys',
475 'timers',
476 'tls',
477 'tty',
478 'url',
479 'util',
480 'vm',
481 'zlib'
482 ]
483 const externalsList = Array.isArray(builtinList) ? builtinList : enumList
484 // 确定是node原生模块,并且没有在package.json 中引用这个模块
485 if (externalsList.indexOf(request) > -1 && projectDependencies.indexOf(request) === -1) {
486 colorconsole.warn(
487 `您当前使用了 ${request} 似乎是 node 原生模块, 快应用不是 node 环境不支持 node 原生模块`
488 )
489 }
490 callback()
491 }
492
493 /**
494 * 校验并返回webpack devtool值(sourcemap)
495 * @param {String} mode - webpack mode
496 * @param {String} devtool - devtool参数值
497 */
498 function getDevtoolValue(mode, devtool) {
499 const sourcemaps = {
500 development: {
501 default: 'cheap-module-eval-source-map',
502 options: [
503 'none',
504 'eval',
505 'cheap-eval-source-map',
506 'cheap-module-eval-source-map',
507 'eval-source-map',
508 'cheap-source-map',
509 'cheap-module-source-map',
510 'inline-cheap-source-map',
511 'inline-cheap-module-source-map',
512 'source-map',
513 'inline-source-map',
514 'hidden-source-map',
515 'nosources-source-map'
516 ]
517 },
518 production: {
519 default: 'none',
520 options: [
521 'none',
522 'cheap-source-map',
523 'cheap-module-source-map',
524 'source-map',
525 'hidden-source-map',
526 'nosources-source-map'
527 ]
528 }
529 }
530 const sourcemapArr = sourcemaps[mode].options
531 const defaultSourcemap = sourcemaps[mode].default
532 if (typeof devtool !== 'string') {
533 return defaultSourcemap
534 }
535 if (sourcemapArr.indexOf(devtool) === -1) {
536 colorconsole.warn(`${mode} 模式 devtool 不支持 '${devtool}', 改为默认 '${defaultSourcemap}'`)
537 return defaultSourcemap
538 }
539 return devtool
540 }
541 return webpackConf
542}