super-project
Version:
Base framework for Super Project.
850 lines (714 loc) • 25.6 kB
JavaScript
process.env.DO_WEBPACK = true
//
const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
//
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const WebpackConfig = require('webpack-config').default
const common = require('./common')
const getAppType = require('../../utils/get-app-type')
const createPWAsw = require('../pwa/create')
const SuperI18nPlugin = require("./plugins/i18n")
const __ = require('../../utils/translate')
const getPort = require('../../utils/get-port')
const spinner = require('../../utils/spinner')
const defaultBuildConfig = require('../../defaults/build-config')
// 调试webpack模式
// const DEBUG = 1
// 程序启动路径,作为查找文件的基础
const RUN_PATH = process.cwd();
// 初始化环境变量
require('../../utils/init-node-env')()
// 用户自定义系统配置
// const SYSTEM_CONFIG = require('../../config/system')
// const DIST_PATH = require('')
process.env.DO_WEBPACK = false
/**
* 修复配置
* 配置有可能是 Array
*
* @param {any} config webpack的配置对象
* @returns 修复后的配置对象
*/
function makeItButter(config) {
// 数组情况,拆分每项分别处理
if (Array.isArray(config))
return config.map(thisConfig => makeItButter(thisConfig))
// no ref obj
config = Object.assign({}, config)
// try to fix a pm2 bug that will currupt [name] value
if (config.output) {
for (let key in config.output) {
if (typeof config.output[key] === 'string')
config.output[key] = config.output[key].replace(/-_-_-_-_-_-(.+?)-_-_-_-_-_-/g, '[name]')
}
}
// remove all undefined from plugins
if (!Array.isArray(config.plugins)) {
config.plugins = []
}
config.plugins = config.plugins.filter(plugin => typeof plugin !== 'undefined')
// remove duplicate plugins
// if (Array.isArray(config.plugins)) {
// config.plugins = removeDuplicateObject(config.plugins)
// }
// remove duplicate rules
if (Array.isArray(config.module.rules)) {
config.module.rules = removeDuplicateObject(config.module.rules)
}
// 删除重复对象
function removeDuplicateObject(list) {
let map = {}
list = (() => {
return list.map((rule) => {
let key = JSON.stringify(rule)
key = key.toLowerCase().replace(/ /g, '')
if (map[key])
rule = undefined
else
map[key] = 1
return rule
})
})()
return list.filter(rule => rule != undefined)
}
// analyze
const isAnalyze = (JSON.parse(process.env.WEBPACK_ANALYZE) || config.analyze) ? true : false
if (isAnalyze) {
config.output.filename = 'entry.[id].[name].js'
config.output.chunkFilename = 'chunk.[id].[name].js'
config.plugins.push(
new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)()
)
}
// custom logic use
delete config.__ext
delete config.spa
delete config.analyzer
delete config.htmlPath
// no ref obj
return config
}
/**
* 根据应用配置生产出一个默认webpack配置
*
* @param {any} opt 应用配置
* @param {any} _path 读取默认配置文件地址,非必须
* @returns
*/
async function createDefaultConfig(opt, _path) {
const {
WEBPACK_BUILD_ENV: ENV,
WEBPACK_BUILD_STAGE: STAGE,
WEBPACK_BUILD_TYPE: TYPE,
} = process.env
// 根据当前环境变量,定位对应的默认配置文件
_path = _path || path.resolve(__dirname, `./defaults/${TYPE}.${STAGE}.${ENV}.js`)
const factory = await getConfigFactory(_path)
const config = await factory(opt)
return config
}
/**
* 获取配置生成的工厂方法
*
* @param {any} path 工厂方法对应的文件路径
* @returns 工厂方法
*/
async function getConfigFactory(path) {
let factory
if (fs.existsSync(path))
factory = await require(path)
else
console.log(`!!! ERROR !!! 没找到对应的配置文件: ${path}`)
return factory
}
const _beforeBuild = async () => {
const {
WEBPACK_BUILD_ENV: ENV,
} = process.env
if (ENV === 'dev')
fs.ensureFileSync(path.resolve(
process.cwd(),
process.env.SUPER_DIST_DIR,
`./server/index.js`
))
}
const _afterBuild = async () => {
// console.log(app)
// console.log('AFTER')
}
/**
* Webpack 运行入口方法
*/
module.exports = async (obj) => {
let {
config,
dist,
aliases,
i18n,
pwa,
devServer,
beforeBuild,
afterBuild,
port,
defines,
template,
} = Object.assign({}, defaultBuildConfig, obj)
const appType = await getAppType()
process.env.SERVER_PORT = getPort(port)
const {
WEBPACK_BUILD_TYPE: TYPE,
WEBPACK_BUILD_ENV: ENV,
WEBPACK_BUILD_STAGE: STAGE,
// WEBPACK_ANALYZE,
WEBPACK_DEV_SERVER_PORT: CLIENT_DEV_PORT,
WEBPACK_BUILD_ONE_BY_ONE: ONE_BY_ONE
// SERVER_DOMAIN,
// SERVER_PORT,
} = process.env
// DEBUG && console.log('============== Webpack Debug =============')
// DEBUG && console.log('Webpack 打包环境:', TYPE, STAGE, ENV)
console.log(
chalk.cyan(' ')
+ chalk.yellowBright('[super/build] ')
+ __('build.build_start', {
type: chalk.green(appType),
stage: chalk.green(STAGE),
env: chalk.green(ENV),
})
)
// webpack 执行用的配置对象
let webpackConfigs = []
// 创建默认rules
const createBaseConfig = async () =>
await common.factory({
aliases,
env: ENV,
stage: STAGE,
spa: false,
defines,
})
{ // 处理打包结果目录
// if (dist.substr(0, 1) === '.') dist = path.resolve(process.cwd(), dist)
// 将打包目录存入环境变量
// 在打包时,会使用 DefinePlugin 插件将该值赋值到 __DIST__ 全部变量中,以供项目内代码使用
process.env.SUPER_DIST_DIR = dist
// 确保打包目录存在
await fs.ensureDir(dist)
}
// chunkmap 文件地址
const pathnameChunkmap = path.resolve(dist, `.public-chunkmap.json`)
// 处理i18n
if (typeof i18n === 'object') {
let type = ENV === 'dev' ? 'redux' : 'default'
let expr = '__'
let locales
let cookieKey
let domain
if (Array.isArray(i18n)) {
locales = [...i18n]
} else {
type = i18n.type || type
expr = i18n.expr || expr
locales = [...i18n.locales || []]
cookieKey = i18n.cookieKey || cookieKey
domain = i18n.domain || domain || undefined
}
if (type === 'store') type = 'redux'
type = type.toLowerCase()
if (STAGE === 'client') {
console.log(
chalk.green('√ ')
+ chalk.yellowBright('[super/build] ')
+ `i18n ` + chalk.yellowBright(`enabled`)
)
console.log(` > type: ${chalk.yellowBright(type)}`)
console.log(` > locales: ${locales.map(arr => arr[0]).join(', ')}`)
}
locales.forEach(arr => {
if (arr[2]) return
arr[1] = fs.readJsonSync(path.resolve(process.cwd(), arr[1]))
arr[2] = true
})
process.env.SUPER_I18N = JSON.stringify(true)
process.env.SUPER_I18N_TYPE = JSON.stringify(type)
process.env.SUPER_I18N_LOCALES = JSON.stringify(locales)
if (cookieKey) process.env.SUPER_I18N_COOKIE_KEY = cookieKey
if (domain) process.env.SUPER_I18N_COOKIE_DOMAIN = domain
i18n = {
type,
expr,
locales,
}
if (ENV === 'dev' && type === 'default') {
console.log(` > We recommend using ${chalk.greenBright('redux')} mode in DEV enviroment.`)
}
} else {
i18n = false
process.env.SUPER_I18N = JSON.stringify(false)
}
// 处理HTML模板(如果有)
if (typeof process.env.SUPER_HTML_TEMPLATE !== 'string' &&
typeof template === 'string'
) {
if (template.substr(0, 2) === './') {
template = await fs.readFile(path.resolve(
process.cwd(), template
))
}
process.env.SUPER_HTML_TEMPLATE = template
}
const args = {
config,
dist,
aliases,
i18n,
pwa,
devServer,
}
await _beforeBuild(args)
console.log(
chalk.cyan('⚑ ')
+ chalk.yellowBright('[super/build] ')
+ `callback: ` + chalk.green('before')
)
if (typeof beforeBuild === 'function') {
await beforeBuild(args)
}
if (typeof config === 'function') config = await config()
if (typeof config !== 'object') config = {}
// 显示loading
const building = spinner(chalk.yellowBright('[super/build] ') + __('build.building'))
const buildingComplete = () => {
building.stop()
console.log(' ')
}
/**
* 处理 Webpack 配置对象
*
* @param {object} custom 合并 Webpack 配置对象
* @returns 合并后的值
*/
const parseConfig = async (config = {}) => {
const baseConfig = await createBaseConfig()
// 合并 module.rules / loaders
if (typeof config.module === 'object') {
if (!Array.isArray(config.module.rules)) {
config.module.rules = [
...baseConfig.module.rules,
]
} else {
if (config.module.rules[0] === true) {
config.module.rules.shift()
} else {
config.module.rules = [
...baseConfig.module.rules,
...config.module.rules
]
}
baseConfig.module.rules = undefined
}
} else {
config.module = {
rules: [
...baseConfig.module.rules
]
}
}
// 合并 plugins
if (STAGE === 'server') {
config.plugins = [
...baseConfig.plugins,
]
} else if (!Array.isArray(config.plugins)) {
// config.plugins = [
// ...baseConfig.plugins,
// ]
} else {
if (config.plugins[0] === true) {
config.plugins.shift()
} else {
config.plugins = [
// ...baseConfig.plugins,
...config.plugins
]
}
baseConfig.plugins = undefined
}
return config
}
/**
* 处理客户端配置文件
* [n个应用] x [m个打包配置] = [webpack打包配置集合]
*/
const handlerClientConfig = async () => {
// 把装载的所有子应用的 webpack 配置都加上
// const appsConfig = await require('../../config/apps')
// for (let appName in appsConfig) {
const handleSingleConfig = async (localeId, localesObj) => {
let opt = {
RUN_PATH,
CLIENT_DEV_PORT,
localeId,
/*APP_KEY: appName */
}
const baseConfig = await createBaseConfig()
delete baseConfig.module.rules
const defaultConfig = await createDefaultConfig(opt)
// let defaultSPAConfig = await createSPADefaultConfig(opt)
const defaultClientEntry = path.resolve(
// RUN_PATH,
// `./system/super3/client`
__dirname,
'../../',
appType,
'./client'
)
// let appConfig = appsConfig[appName]
// 如果没有webpack配置,则表示没有react,不需要打包
// if (!appConfig.webpack) continue
let clientConfigs = config
// 统一转成数组,支持多个client配置
if (!Array.isArray(clientConfigs)) {
clientConfigs = [clientConfigs]
}
for (let clientConfig of clientConfigs) {
const config = new WebpackConfig()
clientConfig = new WebpackConfig()
.merge(baseConfig)
.merge(clientConfig)
// 跟进打包环境和用户自定义配置,扩展webpack配置
if (clientConfig.__ext) {
clientConfig.merge(clientConfig.__ext[ENV])
}
let _defaultConfig = (() => {
let config = Object.assign({}, defaultConfig)
// 如果是SPA应用
// if (clientConfig.spa) {
// config = Object.assign({}, defaultSPAConfig)
// }
return config
})()
// 如果自定义了,则清除默认
if (clientConfig.entry) _defaultConfig.entry = undefined
if (clientConfig.output) _defaultConfig.output = undefined
await parseConfig(clientConfig)
config
.merge(_defaultConfig)
.merge(clientConfig)
if (typeof config.output !== 'object')
config.output = {
// path: path.resolve(dist, `./public/includes`),
// publicPath: 'includes/',
}
if (!config.output.path) {
// config.output.path = path.resolve(dist, `./public`)
config.output.path = path.resolve(dist, `./public/includes`)
config.output.publicPath = 'includes/'
}
if (!config.output.publicPath)
config.output.publicPath = '/'
if (
typeof config.entry === 'object' &&
!config.entry.client
) {
config.entry.client = defaultClientEntry
} else if (config.entry === 'object') {
} else if (typeof config.entry !== 'string') {
config.entry = {
client: defaultClientEntry
}
}
if (localeId && typeof localesObj === 'object') {
config.plugins.unshift(
new SuperI18nPlugin({
stage: STAGE,
localeId,
locales: localesObj,
functionName: i18n.expr,
})
)
} else if (typeof i18n === 'object') {
config.plugins.unshift(
new SuperI18nPlugin({
stage: STAGE,
functionName: i18n.expr,
})
)
}
webpackConfigs.push(config)
}
}
if (typeof i18n === 'object') {
const {
type = 'default'
} = i18n
switch (type) {
case 'redux': {
await handleSingleConfig()
break
}
default: {
for (let arr of i18n.locales) {
await handleSingleConfig(arr[0], arr[1])
}
}
}
} else {
await handleSingleConfig()
}
// }
}
/**
* 处理服务端配置文件
* [n个应用] 公用1个服务端打包配置,并且merge了client的相关配置
* 注:如果客户端的配置有特殊要求或者冲突,则需要手动调整下面的代码
*/
const handlerServerConfig = async () => {
// 服务端需要全部子项目的配置集合
// 先合并全部子项目的配置内容
// 再合并到服务端配置里
// const appsConfig = await require('../../config/apps')
let tempClientConfig = new WebpackConfig()
const defaultServerEntry = [
'babel-core/register',
'babel-polyfill',
path.resolve(
// __dirname, '../start'
__dirname,
'../../',
appType,
'./server'
)
]
if (ENV === 'dev') defaultServerEntry.push('webpack/hot/poll?1000')
// for (let appName in appsConfig) {
// 如果没有webpack配置,则表示没有react,不需要打包
// if (!appsConfig[appName].webpack) continue
let configs = config
if (!Array.isArray(configs))
configs = [configs]
configs.forEach((config) => {
parseConfig(config)
tempClientConfig.merge(config)
})
// }
let opt = { RUN_PATH, CLIENT_DEV_PORT }
const baseConfig = await createBaseConfig()
const defaultConfig = await createDefaultConfig(opt)
let thisConfig = new WebpackConfig()
// 注:在某些项目里,可能会出现下面的加载顺序有特定的区别,需要自行加判断
// 利用每个app的配置,设置 include\exclude 等。
thisConfig
.merge(baseConfig)
.merge(defaultConfig)
.merge({
module: tempClientConfig.module,
resolve: tempClientConfig.resolve,
// plugins: tempClientConfig.plugins,
plugins: tempClientConfig.plugins,
})
// 如果用户自己配置了服务端打包路径,则覆盖默认的
if (dist)
thisConfig.output.path = path.resolve(dist, './server')
if (tempClientConfig.output && tempClientConfig.output.publicPath)
thisConfig.output.publicPath = tempClientConfig.output.publicPath
// if (SYSTEM_CONFIG.WEBPACK_SERVER_OUTPATH)
// config.output.path = path.resolve(RUN_PATH, SYSTEM_CONFIG.WEBPACK_SERVER_OUTPATH)
if (typeof i18n === 'object')
thisConfig.plugins.unshift(
new SuperI18nPlugin({
stage: STAGE,
functionName: i18n.expr,
})
)
thisConfig.entry = defaultServerEntry
// webpackConfigs.push(thisConfig)
webpackConfigs = thisConfig
}
const after = async (app) => {
const theArgs = {
app,
...args
}
console.log(' ')
// if (STAGE === 'server' && ENV === 'dev') {
// if (!global.__SUPER_DEV_SERVER_OPN__) {
// opn(`http://${SERVER_DOMAIN || 'localhost'}:${SERVER_PORT}/`)
// global.__SUPER_DEV_SERVER_OPN__ = true
// }
// }
if (pwa && STAGE === 'client' && ENV === 'prod') {
// 生成PWA使用的 service-worker.js
await createPWAsw(pwa, i18n)
}
await _afterBuild(theArgs)
console.log(
chalk.cyan('⚑ ')
+ chalk.yellowBright('[super/build] ')
+ `callback: ` + chalk.green('after')
)
if (typeof afterBuild === 'function')
await afterBuild(theArgs)
console.log(
chalk.green('√ ')
+ chalk.yellowBright('[super/build] ')
+ __('build.build_complete', {
type: chalk.green(appType),
stage: chalk.green(STAGE),
env: chalk.green(ENV),
})
)
if (ENV === 'dev')
console.log(` > ${(new Date()).toLocaleString()}`)
return
}
const logConfigToFile = async () => {
await fs.ensureDir(
path.resolve(
RUN_PATH,
`./logs/webpack-config`
)
)
await fs.writeFile(
path.resolve(
RUN_PATH,
`./logs/webpack-config/${TYPE}.${STAGE}.${ENV}.${(new Date()).toISOString().replace(/:/g, '_')}.json`
),
JSON.stringify(webpackConfigs, null, '\t'),
'utf-8'
)
// DEBUG && console.log('执行配置:')
// DEBUG && console.log('-----------------------------------------')
// DEBUG && console.log(JSON.stringify(webpackConfigs))
// DEBUG && console.log('============== Webpack Debug End =============')
return
}
// 客户端开发模式
if (STAGE === 'client' && ENV === 'dev') {
await handlerClientConfig()
await logConfigToFile()
const compiler = webpack(makeItButter(webpackConfigs))
buildingComplete()
const devServerConfig = Object.assign({
quiet: false,
stats: { colors: true },
hot: true,
inline: true,
contentBase: './',
publicPath: '/dist/',
headers: {
'Access-Control-Allow-Origin': '*'
},
after,
open: TYPE === 'spa',
}, devServer)
// more config
// http://webpack.github.io/docs/webpack-dev-server.html
const server = new WebpackDevServer(compiler, devServerConfig)
server.listen(CLIENT_DEV_PORT)
}
// 客户端打包
if (STAGE === 'client' && ENV === 'prod') {
await fs.pathExists(pathnameChunkmap)
.then(async(exists) => {
if(!exists){
await fs.ensureFile(pathnameChunkmap)
await fs.writeJson(
pathnameChunkmap,
{},
{
spaces: 4
}
)
}
})
// 如果不是一个接一个打包,则清空json文件
if(!ONE_BY_ONE){
await fs.writeJson(
pathnameChunkmap,
{},
{
spaces: 4
}
)
}
// process.env.NODE_ENV = 'production'
await handlerClientConfig()
await logConfigToFile()
// 执行打包
let configs = makeItButter(webpackConfigs)
if (!Array.isArray(configs)) configs = [configs]
for (let config of configs) {
const compiler = webpack(config)
await new Promise((resolve, reject) => {
compiler.run(async (err, stats) => {
if (err) reject(`webpack error: [${TYPE}-${STAGE}-${ENV}] ${err}`)
buildingComplete()
console.log(stats.toString({
chunks: false, // 输出精简内容
colors: true
}))
setTimeout(() => resolve(), 500)
})
})
}
await after()
return
}
// 服务端开发环境
if (STAGE === 'server' && ENV === 'dev') {
await handlerServerConfig()
await logConfigToFile()
await webpack(
makeItButter(webpackConfigs),
async (err, stats) => {
if (err) console.log(`webpack error: [${TYPE}-${STAGE}-${ENV}] ${err}`)
buildingComplete()
console.log(stats.toString({
chunks: false,
colors: true
}))
await after()
}
)
return
}
// 服务端打包
if (STAGE === 'server' && ENV === 'prod') {
// process.env.NODE_ENV = 'production'
// process.env.WEBPACK_SERVER_PUBLIC_PATH =
// (typeof webpackConfigs.output === 'object' && webpackConfigs.output.publicPath)
// ? webpackConfigs.output.publicPath
// : ''
if (!fs.pathExistsSync(pathnameChunkmap)) {
await fs.ensureFile(pathnameChunkmap)
process.env.WEBPACK_CHUNKMAP = ''
console.log(chalk.green('√ ') + chalk.greenBright('Chunkmap') + ` file does not exist. Crated an empty one.`)
} else {
try {
process.env.WEBPACK_CHUNKMAP = JSON.stringify(await fs.readJson(pathnameChunkmap))
} catch (e) {
process.env.WEBPACK_CHUNKMAP = ''
}
}
await handlerServerConfig()
await logConfigToFile()
await new Promise((resolve, reject) => {
webpack(makeItButter(webpackConfigs), async (err, stats) => {
if (err) reject(`webpack error: [${TYPE}-${STAGE}-${ENV}] ${err}`)
buildingComplete()
console.log(stats.toString({
chunks: false, // Makes the build much quieter
colors: true
}))
resolve()
})
})
await after()
return
}
}
// justDoooooooooooooIt()