cyb-cli
Version:
塞伯坦CYB-CLI是面向前端模块化工程的构建工具。主要目的是帮助开发者统一前端开发模式和项目开发结构,提高功能扩展和降低维护成本,自动化前端工作流,提高开发效率和开发质量。
730 lines (676 loc) • 19.6 kB
JavaScript
/**
* =================================
* @2018 塞伯坦-CYB前端模块化工程构建工具
* https://github.com/jd-cyb/cyb-cli
* =================================
*/
/**
* ---------------------------------
* 研发任务
* ---------------------------------
*/
const path = require('path')
const fs = require('fs')
const fancyLog = require('fancy-log')
const chalk = require('chalk')
const del = require('del')
const globWatcher = require('glob-watcher')
const vfs = require('vinyl-fs')
const injectString = require('gulp-inject-string')
const gulpReplace = require('gulp-replace')
const mainBowerFiles = require('main-bower-files')
const filter = require('gulp-filter')
const flatten = require('gulp-flatten')
const inject = require('gulp-inject')
const lazypipe = require('lazypipe')
const es = require('event-stream')
const rename = require('gulp-rename')
const gulpif = require('gulp-if')
const less = require('gulp-less')
const sass = require('gulp-sass')
const stylus = require('gulp-stylus')
const postcss = require('gulp-postcss')
const postcssAutoprefixer = require('autoprefixer')
const postcssPxtorem = require('postcss-pxtorem')
const plumber = require('gulp-plumber')
const notify = require('gulp-notify')
const buffer = require('vinyl-buffer')
const sourcemaps = require('gulp-sourcemaps')
const bs = require('browser-sync')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const async = require('async')
const glob = require('glob')
const config = require('./lib/fezconfig')
const zIndex = require('./lib/zindex')
const compileJs = require('./lib/webpack')
const proxy = require('http-proxy-middleware')
module.exports = () => {
console.log(chalk.cyan('Starting dev for development...'))
/**
* 调用browsersync自动刷新浏览器
*/
const reloadHandler = () => {
config.browsersync.dev.available && bs.reload()
}
/**
* 通用复制模块
*/
const copyHandler = (type, file = '**/*') => {
return new Promise((resolve, reject) => {
vfs.src(path.join(process.cwd(), config.paths.src[type], file), {
base: config.paths.src.dir
})
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(vfs.dest(config.paths.dev.dir))
.on('end', () => {
reloadHandler()
resolve()
})
})
}
/**
* postcss插件配置
*/
const postcssOption = [postcssAutoprefixer(config.autoprefixer.options)]
/**
* 检测bower是否可用
*/
const bowerAvailable = () => {
if (!config.merge.vendor.available) return false
if (!fs.existsSync(path.join(process.cwd(), 'bower_components'))) return false
if (mainBowerFiles().length <= 0) return false
return true
}
/**
* 删除开发目录
*/
function delDev(cb) {
del([config.paths.dev.dir])
.then(() => {
cb()
})
}
/**
* 复制图片到研发目录
*/
function copyImages(cb) {
if (!fs.existsSync(path.join(process.cwd(), config.paths.src.img))) return cb()
copyHandler('img')
.then(() => {
cb()
})
}
/**
* 复制公共脚本到研发目录
*/
function copyLib(cb = () => {}) {
if (!fs.existsSync(path.join(process.cwd(), config.paths.src.common))) return cb()
copyHandler("common")
.then(() => {
cb()
})
}
/**
* 复制字体到研发目录
*/
function copyFonts(cb) {
if (!fs.existsSync(path.join(process.cwd(), config.paths.src.fonts))) return cb()
copyHandler('fonts')
.then(() => {
cb()
})
}
/**
* 复制自定义文件到研发目录
*/
function copyCustom(cb = () => {}) {
if (!fs.existsSync(path.join(process.cwd(), config.paths.src.custom))) return cb()
copyHandler('custom')
.then(() => {
cb()
})
}
/**
* 复制vendor文件到研发目录
*/
function copyVendor(cb = () => {}) {
if (!fs.existsSync(path.join(process.cwd(), config.paths.src.vendor))) return cb()
copyHandler('vendor')
.then(() => {
cb()
})
}
/**
* 编译css
*/
function compileCss(cb = () => {}) {
if (glob.sync(`${config.paths.src.styles}/*.css`).length === 0) return cb()
vfs.src(`${config.paths.src.styles}/*.css`)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
//css中的rem转换
.pipe(gulpif(
config.useREM.css.available,
postcss([
postcssPxtorem(Object.assign({}, config.useREM.css.options))
])
))
.pipe(postcss(postcssOption)) //添加CSS前缀
.pipe(vfs.dest(config.paths.dev.css))
.on('end', () => {
reloadHandler()
cb()
})
}
/**
* 编译less
*/
function compileLess(cb = () => {}) {
if (glob.sync(`${config.paths.src.styles}/*.less`).length === 0) return cb()
vfs.src(`${config.paths.src.styles}/*.less`)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(sourcemaps.init())
.pipe(less(Object.assign({
relativeUrls: true //将网址编译成相对网址
}, config.style.lessOptions)))
.on('error', (error) => {
fancyLog(error.message)
})
//css中的rem转换
.pipe(gulpif(
config.useREM.css.available,
postcss([
postcssPxtorem(Object.assign({}, config.useREM.css.options))
])
))
.pipe(postcss(postcssOption)) //添加CSS前缀
.pipe(sourcemaps.write())
.pipe(vfs.dest(config.paths.dev.css))
.on('end', () => {
reloadHandler()
cb()
})
}
/**
* 编译sass
*/
function compileSass(cb = () => {}) {
if (glob.sync(`${config.paths.src.styles}/*.{scss,sass}`).length === 0) return cb()
vfs.src(`${config.paths.src.styles}/*.{scss,sass}`)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(sourcemaps.init())
.pipe(sass(Object.assign({
/**
* ------- outputStyle 取值 ------
* nested:嵌套缩进的css代码,它是默认值。
* expanded:没有缩进的、扩展的css代码。
* compact:简洁格式的css代码。
* compressed:压缩后的css代码
*/
outputStyle: 'compact'
}, config.style.sassOptions)))
.on('error', (error) => {
fancyLog(error.message)
})
//css中的rem转换
.pipe(gulpif(
config.useREM.css.available,
postcss([
postcssPxtorem(Object.assign({}, config.useREM.css.options))
])
))
.pipe(postcss(postcssOption)) //添加CSS前缀
.pipe(sourcemaps.write())
.pipe(vfs.dest(config.paths.dev.css))
.on('end', () => {
reloadHandler()
cb()
})
}
/**
* 编译stylus
*/
function compileStylus(cb = () => {}) {
if (glob.sync(`${config.paths.src.styles}/*.styl`).length === 0) return cb()
vfs.src(`${config.paths.src.styles}/*.styl`)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(sourcemaps.init())
.pipe(stylus(Object.assign({}, config.style.stylusOptions)))
.on('error', (error) => {
fancyLog(error.message)
})
//css中的rem转换
.pipe(gulpif(
config.useREM.css.available,
postcss([
postcssPxtorem(Object.assign({}, config.useREM.css.options))
])
))
.pipe(postcss(postcssOption)) //添加CSS前缀
.pipe(sourcemaps.write())
.pipe(vfs.dest(config.paths.dev.css))
.on('end', () => {
reloadHandler()
cb()
})
}
/**
* 复制bower文件到dev目录
* 研发环境直接使用 bower 路径 不对文件作任何处理
*/
function copyBowerFiles(cb) {
if (!bowerAvailable()) return cb()
const cssFilter = filter('**/*.css', {
restore: true
})
vfs.src(mainBowerFiles(), {
base: './'
})
.pipe(cssFilter)
//css中的rem转换
.pipe(gulpif(
config.useREM.css.available,
postcss([
postcssPxtorem(Object.assign({}, config.useREM.css.options))
])
))
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(cssFilter.restore)
.pipe(vfs.dest(config.paths.dev.dir))
.on('end', () => {
cb()
})
}
const injectHtmlFiles = (htmlPath = path.join(process.cwd(), config.paths.src.html, '**/*.html')) => {
return new Promise((resolve, reject) => {
/**
* 自动注入Bower库文件到html页面中
*/
const injectBower = lazypipe()
.pipe(() => {
if (!bowerAvailable()) return buffer()
return inject(vfs.src(mainBowerFiles(), {
read: false
}), {
starttag: '<!-- inject:vendor:{{ext}} -->',
name: "vendor",
relative: true,
ignorePath: '../../../',
quiet: true
// addRootSlash: true
})
})
/**
* 自动注入项目公共库文件到html页面中
*/
const injectLib = lazypipe()
.pipe(() => {
return inject(vfs.src([`./dev/static/css/**/common*.css`, `./dev/common/**/*.js`, `!./dev/common/**/assign-*.js`], {
read: false
}), {
starttag: '<!-- inject:common:{{ext}} -->',
relative: true,
ignorePath: '../../../dev/',
quiet: true
// addRootSlash: true
})
})
const injectHtml = (es) => {
return es.map((file, cb) => {
const cateName = file.path.match(/((.*?)[\/|\\])*([^.]+).*/)[2]
vfs.src(file.path)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(rename(cateName + '.html'))
.pipe(gulpif(
bowerAvailable(),
injectBower()
))
.pipe(gulpif(
config.merge.common.available,
injectLib()
))
.pipe(gulpif(
config.merge.page,
inject(vfs.src([`./dev/common/**/assign*-${cateName}*.js`, `./dev/**/css/${cateName}.css`, `./dev/**/css/${cateName}/index.css`, `./dev/**/js/${cateName}.js`], {
read: false
}), {
starttag: '<!-- inject:page:{{ext}} -->',
relative: true,
ignorePath: '../../../dev/',
quiet: true
// addRootSlash: true
})
))
//页面对应的JS脚本通过webpack保存在内存中不再实际生成保存在dev目录
.pipe(injectString.after('<!-- inject:page:js -->', `\n<script src="static/js/${cateName}.js"></script>`))
.pipe(vfs.dest(config.paths.dev.html))
.on("end", () => {
cb()
})
})
}
/**
* 入口页面
*/
const indexHtmlFilter = filter(`**/index.html`, {
restore: true
})
vfs.src(htmlPath)
.pipe(plumber({
errorHandler: notify.onError("Error: <%= error.message %>")
}))
.pipe(indexHtmlFilter)
.pipe(injectHtml(es))
.pipe(indexHtmlFilter.restore)
.pipe(vfs.dest(config.paths.dev.html))
.on('end', () => {
reloadHandler()
resolve()
})
})
}
/**
* 编译 html 文件
*/
function handleHtml(cb) {
injectHtmlFiles().then(() => {
cb()
})
}
/**
* 使用gulp监听文件改动
*/
function watch(cb) {
/**
* 通用处理文件改动
*/
const watchHandler = (type, file) => {
const target = file.match(/^src[\/|\\](.*?)[\/|\\]/)[1]
const pathParse = path.parse(file)
if (target === "views") {
if (pathParse.ext === '.html') {
const removeFiles = file.match(/^src[\/|\\]views[\/|\\](.*?)[\/|\\]/)[1]
/*监视页面*/
if (type === 'removed') {
del([`${config.paths.dev.dir}/**/${removeFiles}.html`, `${config.paths.dev.dir}/**/${removeFiles}.js`]).then(() => {
setTimeout(function() {
findAllHtml()
}, 500)
})
} else if (type === 'add') {
injectHtmlFiles(file).then(() => {
setTimeout(function() {
findAllHtml()
}, 500)
})
} else {
injectHtmlFiles(file)
}
} else if (pathParse.ext === '.css' && pathParse.name === 'index') {
compileCss()
} else if (pathParse.ext === '.less' && pathParse.name === 'index') {
compileLess()
} else if ((pathParse.ext === '.scss' || pathParse.ext === '.sass') && pathParse.name === 'index') {
compileSass()
} else if (pathParse.ext === '.styl' && pathParse.name === 'index') {
compileStylus()
}
} else if (target === "common") {
copyLib()
} else if (target === "custom") {
copyCustom()
} else if (target === "vendor") {
copyVendor()
} else if (target === "static") {
/*监视静态资源*/
const staticFile = file.match(/^src[\/|\\]static[\/|\\](.*?)[\/|\\]/)[1]
switch (staticFile) {
case 'images':
if (type === 'removed') {
const tmp = `${config.paths.dev.img}/**/${pathParse.base}`
del([tmp])
} else {
copyHandler('img', path.join('**/', path.basename(file)))
}
break
case 'fonts':
if (type === 'removed') {
const tmp = `${config.paths.dev.fonts}/**/${pathParse.base}`
del([tmp])
} else {
copyHandler('fonts', path.join('**/', path.basename(file)))
}
break
case 'styles':
if (type === 'removed') {
const tmp = `${config.paths.dev.css}/**/${pathParse.name}`
del([tmp])
} else {
switch (pathParse.ext) {
case '.css':
compileCss()
break
case '.less':
compileLess()
break
case '.scss':
case '.sass':
compileSass()
break
case '.styl':
compileStylus()
break
}
}
break
}
}
}
const watcher = globWatcher([
config.paths.src.dir + '/**/*'
], {
ignored: /[\/\\]\./
})
watcher
.on('change', (file) => {
fancyLog(`${file} have been changed.`)
watchHandler('changed', file)
})
.on('add', (file) => {
fancyLog(`${file} have been added.`)
watchHandler('add', file)
})
.on('unlink', (file) => {
fancyLog(`${file} have been deleted.`)
watchHandler('removed', file)
})
cb()
}
/**
* 罗列所有html页面
*/
function findAllHtml(cb = () => {}) {
zIndex()
.then(() => {
cb()
})
}
/**
* 处理JS并启动服务
*/
function handleJs(cb) {
/**
* 启动 browsersync
* 配置参考:http://www.browsersync.cn/docs/options/
*/
const startServer = (webpackCompiled, cb) => {
let proxyMiddleware = []
if (Object.prototype.toString.call(config.proxy) === "[object Array]") {
for (let item of config.proxy) {
let proxyItem = proxy(item.context, item.options)
proxyMiddleware.push(proxyItem)
}
} else if (Object.prototype.toString.call(config.proxy) === "[object Object]") {
let proxyItem = proxy(config.proxy.context, config.proxy.options)
proxyMiddleware.push(proxyItem)
}
bs.create('cyb dev')
bs.init(Object.assign({
//在Chrome浏览器中打开网站
// open: "external",
// browser: "google chrome",
socket: {
namespace: `/cyb-cli-dev-${config.projectName}`
},
server: {
baseDir: config.paths.dev.dir
},
middleware: [
webpackDevMiddleware(webpackCompiled.compiler, {
publicPath: webpackCompiled.webpackConfig.output.publicPath,
watchOptions: {
ignored: /node_modules/
},
logLevel: 'silent',
stats: {
context: path.relative(__dirname, '../'),
colors: true
}
}),
webpackHotMiddleware(webpackCompiled.compiler),
...(proxyMiddleware)
],
callbacks: {
ready: function(err, bs) {
require('./lib/mock-middleware')().filter((item) => {
bs.addMiddleware(item.route, item.handle)
})
}
},
startPath: '/',
notify: { //提醒条样式
styles: [
"margin: 0",
"padding: 5px 10px",
"position: fixed",
"font-size: 14px",
"z-index: 9999",
"bottom: 0px",
"right: 0px",
"border-radius: 0",
"border-top-left-radius: 8px",
"background-color: rgba(0,0,0,0.5)",
"color: white",
"text-align: center"
]
}
}, config.browsersync.dev.options), () => {
cb()
})
}
compileJs.dev()
.then(result => {
startServer(result, cb)
})
.catch(error => {
console.log(error)
})
}
/**
* 研发任务
* series 中的任务同步执行
* parallel 中的任务异步执行
*/
async.series([
function(next) {
delDev(next)
},
function(next) {
async.parallel([
function(cb) {
compileCss(cb)
},
function(cb) {
compileLess(cb)
},
function(cb) {
compileSass(cb)
},
function(cb) {
compileStylus(cb)
},
function(cb) {
copyImages(cb)
},
function(cb) {
copyFonts(cb)
},
function(cb) {
copyLib(cb)
},
function(cb) {
copyCustom(cb)
},
function(cb) {
copyVendor(cb)
}
], function(error) {
if (error) {
throw new Error(error)
}
next()
})
},
function(next) {
async.parallel([
function(cb) {
copyBowerFiles(cb)
},
function(cb) {
handleHtml(cb)
}
], function(error) {
if (error) {
throw new Error(error)
}
next()
})
},
function(next) {
async.parallel([
function(cb) {
watch(cb)
},
function(cb) {
findAllHtml(cb)
},
function(cb) {
handleJs(cb)
}
], function(error) {
if (error) {
throw new Error(error)
}
next()
})
}
], function(err) {
if (err) {
throw new Error(err)
}
})
}