lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
727 lines (634 loc) • 22.9 kB
JavaScript
/*
* LuLu UI开发node.js脚本
* 1. 实时CSS合并;
* 2. 实时JS合并;
* 3. 实时html编译;
*/
/* global console, process */
const fs = require('fs');
const path = require('path');
const url = require('url');
const http = require('http');
const https = require('https');
const os = require('os');
const child_process = require('child_process');
let args = process.argv.splice(2);
if (args.length == 0) {
args = ['pure', 'edge', 'hope'];
}
String.prototype.theme = function (theme) {
return this.replace('${theme}', theme || 'pure');
};
/*
** 文件合并方法
** @params arrUrls Array 需要合并的文件或文件夹(文件夹会合并其中所有1级文件)
** @params strUrl String 合并后的文件名称
*/
const combo = function (arrUrls, strUrl, filter) {
var content = '';
if (fs.existsSync(strUrl) == false) {
console.error(strUrl + '不存在,合并失败');
return;
}
// 遍历url并读取文件内容
if (arrUrls && arrUrls.length && strUrl) {
arrUrls.forEach(function (url) {
if (fs.existsSync(url) == false) {
return;
}
let st = fs.statSync(url);
if (st.isFile()) {
// 如果是文件
content += fs.readFileSync(url);
} else if (st.isDirectory()) {
// 作为文件夹
fs.readdirSync(url).forEach(function (filename) {
let dir = path.join(url, filename);
if (fs.statSync(dir).isFile()) {
content += fs.readFileSync(dir);
}
});
}
});
if (typeof filter == 'function') {
content = filter(content);
}
// 写入新目录
// 写入项目配置数据
fs.writeFile(strUrl, content.trim(), function () {
console.log('资源合并为' + strUrl + '成功');
});
}
};
/*
** 删除文件极其目录方法
** @src 删除的目录
*/
const clean = function (src) {
if (!fs.existsSync(src)) {
return;
}
// 读取目录中的所有文件/目录
var paths = fs.readdirSync(src);
paths.forEach(function (dir) {
let _src = path.join(src, dir);
let st = fs.statSync(_src);
if (st.isFile()) {
// 如果是文件,则删除
fs.unlinkSync(_src);
} else if (st.isDirectory()) {
// 作为文件夹
clean(_src);
}
});
// 删除文件夹
try {
fs.rmdirSync(src);
console.log('已清空文件夹' + src);
} catch(e) {}
};
/*
** 创建路径对应的文件夹(如果没有)
** @params path 目标路径
*/
const createPath = function (path) {
// 路径有下面这几种
// 1. /User/... OS X
// 2. E:/mydir/... window
// 3. a/b/... 下面3个相对地址,与系统无关
// 4. ./a/b/...
// 5. ../../a/b/...
path = path.replace(/\\/g, '/');
var pathHTML = '.';
if (path.slice(0, 1) == '/') {
pathHTML = '/';
} else if (/:/.test(path)) {
pathHTML = '';
}
path.split('/').forEach(function (filename) {
if (filename) {
// 如果是数据盘地址,忽略
if (/:/.test(filename) == false) {
pathHTML = pathHTML + '/' + filename;
// 如果文件不存在
if (!fs.existsSync(pathHTML)) {
console.log('路径' + pathHTML + '不存在,新建之');
fs.mkdirSync(pathHTML);
}
} else {
pathHTML = filename;
}
}
});
};
/*
* 复制目录中的所有文件包括子目录
* @param{ String } 需要复制的目录
* @param{ String } 复制到指定的目录
*/
const copy = function (src, dst) {
if (!fs.existsSync(src)) {
return;
}
if (!fs.existsSync(dst)) {
fs.mkdirSync(dst);
}
// 读取目录中的所有文件/目录
var paths = fs.readdirSync(src);
paths.forEach(function (dir) {
var _src = path.join(src, dir);
var _dst = path.join(dst, dir);
var readable, writable;
let st = fs.statSync(_src);
// 判断是否为文件
if (st.isFile()) {
// 创建读取流
readable = fs.createReadStream(_src);
// 创建写入流
writable = fs.createWriteStream(_dst);
// 通过管道来传输流
readable.pipe(writable);
} else if (st.isDirectory()) {
// 作为文件夹处理
createPath(_dst);
copy(_src, _dst);
}
});
};
/**
* 获取线上资源
* @param {*}
* @param {*} success
* @param {*} error
*/
const getHttpsData = function (url, success = function () {}, error = function () {}) {
https.get(url, function (res) {
var statusCode = res.statusCode;
if (statusCode !== 200) {
// 出错回调
error();
// 消耗响应数据以释放内存
res.resume();
return;
}
res.setEncoding('utf8');
var rawData = '';
res.on('data', function (chunk) {
rawData += chunk;
});
// 请求结束
res.on('end', function () {
// 成功回调
success(rawData);
}).on('error', function (e) {
// 出错回调
error();
});
});
};
/*
** 文档同步更新的方法
*/
const ayncDocs = function () {
// 遍历文件夹下的文件
const dirDocs = './docs';
// 如果不存在docs文件,则新建之
if (!fs.existsSync(dirDocs)) {
fs.mkdirSync(dirDocs);
}
// 获取当前本地文档的版本
let version = '';
let pathVersiontxt = path.join(dirDocs, 'version.txt');
if (fs.existsSync(pathVersiontxt)) {
version = fs.readFileSync(pathVersiontxt, 'utf-8');
}
// 拉取线上资源
let versionOnline = '0';
console.log('正在获取文档版本...');
getHttpsData('https://raw.githubusercontent.com/yued-fe/lulu/gh-pages/version.txt', function (txt) {
versionOnline = txt;
console.log('文档版本文件获取成功,版本是:' + versionOnline);
// 和线上文档版本不一致
if (versionOnline !== version) {
// 提醒下,就不强制更新了,除非本地文档是空的
if (version) {
console.warn('本地文档和线上文档版本不一致,如要更新,可以删除docs文件夹,再执行一遍node run');
} else {
// 使用 git 指令把 文档分支中的文件复制过来
console.log('开始复制文档资源...');
// 复制文档
child_process.exec('git show gh-pages:index.html > ' + path.join(dirDocs, 'index.html'), function (err) {
if (err) {
console.error(`exec error: ${err}`);
return;
}
console.log('index.html复制成功');
});
// 需要复制的资源
const sourceCopy = ['pure', 'edge', 'hope'];
let indexCopy = 0;
const stepCopy = function () {
const dir = sourceCopy[indexCopy];
if (!dir) {
setTimeout(() => {
// 清除这几个文件
sourceCopy.forEach(dir => clean(dir));
}, 200);
// 写入版本
if (version === '') {
fs.writeFile(pathVersiontxt, versionOnline, (err) => {
if (err) {
console.error(`exec error: ${err}`);
return;
}
console.log('版本文件写入成功');
});
}
return;
}
// 遍历复制
child_process.exec('git checkout gh-pages -- ' + dir, function (err) {
if (err) {
console.error(`exec error: ${err}`);
return;
}
copy(dir, path.join(dirDocs, dir));
console.log(dir + '复制成功');
// 下一个文件夹复制
indexCopy++;
stepCopy();
});
};
stepCopy();
}
}
});
};
const pathThemeCSS = './theme/${theme}/css/common';
const pathThemeJS = './theme/${theme}/js/common';
const pathThemeDocsSrc = './docs-edit/${theme}';
const pathThemeDocsDist = './docs/${theme}';
// 任务
const task = {
css: {
common: function (pathCSS) {
var arrPathSrc = [pathCSS + '/ui', pathCSS + '/comp'];
if (/edge/i.test(pathCSS)) {
arrPathSrc.unshift(pathCSS + '/variables.css');
}
// 合并为common.css
combo(arrPathSrc, pathCSS + '/ui.css', function (css) {
return '@charset "UTF-8";' + css.replace(/@charset "UTF-8";/g, '');
});
},
form: function (pathCSS) {
// 合并为form.css
combo([
pathCSS + '/variables.css',
pathCSS + '/ui/Button.css',
pathCSS + '/ui/Checkbox.css',
pathCSS + '/ui/Color.css',
pathCSS + '/ui/Date.css',
pathCSS + '/ui/Input.css',
pathCSS + '/ui/Placeholder.css',
pathCSS + '/ui/Progress.css',
pathCSS + '/ui/Radio.css',
pathCSS + '/ui/Range.css',
pathCSS + '/ui/Select.css',
pathCSS + '/ui/Switch.css',
pathCSS + '/ui/Textarea.css'
], pathCSS + '/form.css', function (css) {
return '@charset "UTF-8";' + css.replace(/@charset "UTF-8";/g, '');
});
},
init: function (theme) {
let pathCSS = pathThemeCSS.theme(theme);
// 资源清理
this.common(pathCSS);
this.form(pathCSS);
}
},
js: {
common: function (pathJS, theme) {
// ui组建合并为all.js
combo([
pathJS + '/ui/Keyboard.js',
pathJS + '/ui/Follow.js',
pathJS + '/ui/Tab.js',
pathJS + '/ui/Select.js',
pathJS + '/ui/Drop.js',
pathJS + '/ui/Tips.js',
pathJS + '/ui/LightTip.js',
pathJS + '/ui/ErrorTip.js',
pathJS + '/ui/Loading.js',
pathJS + '/ui/Range.js',
pathJS + '/ui/Color.js',
pathJS + '/ui/Dialog.js',
pathJS + '/ui/Datalist.js',
pathJS + '/ui/DateTime.js',
pathJS + '/ui/Validate.js',
pathJS + '/ui/Pagination.js',
pathJS + '/comp/Table.js',
pathJS + '/comp/Form.js'
], pathJS + '/all.js', function (js) {
// 过滤import和export语句
if (theme == 'edge') {
return js.replace(/^(import|export) /gm, '// $1 ').replace(/^\/\*\*/gm, '\n/**');
}
return js.replace(/\s+if \(typeof exports ===[\w\W]+?\} else \{([\w\W]+?)\}\s+/gm, '$1').replace(/\s+if \(typeof require ===? [\w\W]+? return \{\};(\r|\n)+\s+}/gm, '').replace(/\/\* global module \*\//g, '').replace(/, function \(require\) \{/g, ', function () {').replace(/\s{8}(\w[\w\W]+?factory)/g, ' $1');
});
},
init: function (theme) {
let pathJS = pathThemeJS.theme(theme);
// 两个合并
this.common(pathJS, theme);
}
}
};
const HOPE_ROOT = './theme/hope/ui/';
const taskHope = {
css () {
let arrUrls = [
HOPE_ROOT + 'variables.css',
HOPE_ROOT + 'reset.css'
];
// 再加入各个文件夹下的 CSS 文件
fs.readdirSync(HOPE_ROOT).forEach(function (filename) {
let dir = path.join(HOPE_ROOT, filename);
if (fs.statSync(dir).isDirectory()) {
// 作为文件夹
fs.readdirSync(dir).forEach(function (filename) {
if (/css$/.test(filename)) {
let file = path.join(dir, filename);
if (fs.statSync(file).isFile()) {
arrUrls.push(file);
}
}
});
}
});
// 合并为common.css
combo(arrUrls, HOPE_ROOT + 'ui.css', function (css) {
return '@charset "UTF-8";' + css.replace(/@charset "UTF-8";/gi, '').replace(/@import[\w\W]+?;/gi, '');
});
},
// hope JS 合并
js () {
let arrUrls = [];
// 再加入各个文件夹下的 CSS 文件
fs.readdirSync(HOPE_ROOT).forEach(function (filename) {
let dir = path.join(HOPE_ROOT, filename);
if (fs.statSync(dir).isDirectory()) {
// 作为文件夹
fs.readdirSync(dir).forEach(function (filename) {
if (/js$/.test(filename)) {
let file = path.join(dir, filename);
if (fs.statSync(file).isFile()) {
arrUrls.push(file);
}
}
});
}
});
// 合并为common.css
combo(arrUrls, HOPE_ROOT + 'all.js', function (js) {
return `/*JS 合集*/
(async () => {
if (!CSS.supports('overflow-anchor:auto') || !CSS.supports('offset:none')) {
await import('./safari-polyfill.js');
}` + js.replace(/\/\*\*\/[\w\W]+?\n/g, '').replace(/\/\*[\w\W]+?\*\//gm, '') + `})();`;
});
}
};
// 一开始第一次任务
args.forEach(function (theme) {
console.log('开始' + theme + '主题的初始任务...');
if (theme != 'hope') {
for (var keyTask in task) {
task[keyTask].init(theme);
}
} else {
taskHope.css();
taskHope.js();
}
});
// 文档获取
ayncDocs();
// 开启watch任务
// CSS监控任务
let timerCommonCSS;
let timerUIJS;
args.forEach(function (theme) {
if (theme == 'hope') {
// hope 主题 CSS 和 JS 在一个文件夹下面,所以监控任务是一起的
fs.watch(HOPE_ROOT, {
recursive: true
}, (eventType, filename) => {
if (/css$/.test(filename) && filename.indexOf('ui') == -1) {
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerCommonCSS);
console.log(filename + '发生了' + eventType + '变化');
timerCommonCSS = setTimeout(() => {
console.log('ui.css合并...');
taskHope.css();
}, 100);
} else if (/js$/.test(filename) && filename.indexOf('all') == -1) {
clearTimeout(timerUIJS);
console.log(filename + '发生了' + eventType + '变化');
timerUIJS = setTimeout(() => {
console.log('all.js合并...');
taskHope.js();
}, 100);
}
});
return;
}
let pathCSS = pathThemeCSS.theme(theme);
fs.watch(pathCSS + '/ui', {
recursive: true
}, (eventType, filename) => {
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerCommonCSS);
console.log(filename + '发生了' + eventType + '变化');
timerCommonCSS = setTimeout(() => {
console.log('ui.css合并...');
task.css.common(pathCSS);
console.log('form.css合并...');
task.css.form(pathCSS);
}, 100);
});
fs.watch(pathCSS + '/comp', {
recursive: true
}, (eventType, filename) => {
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerCommonCSS);
console.log(filename + '发生了' + eventType + '变化');
timerCommonCSS = setTimeout(() => {
console.log('ui.css合并...');
task.css.common(pathCSS);
}, 100);
});
if (theme == 'edge') {
fs.watchFile(path.join(pathCSS, 'variables.css'), () => {
// index.html顺便复制下
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerCommonCSS);
timerCommonCSS = setTimeout(() => {
console.log('variables.css发生了变化, ui.css合并...');
task.css.common(pathCSS);
}, 100);
});
}
});
args.forEach(function (theme) {
if (theme == 'hope') {
// hope 主题 CSS 和 JS 在一个文件夹下面,所以监控任务是一起的
return;
}
let pathJS = pathThemeJS.theme(theme);
fs.watch(pathJS + '/ui', {
recursive: true
}, (eventType, filename) => {
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerUIJS);
console.log(filename + '发生了' + eventType + '变化');
timerUIJS = setTimeout(() => {
console.log('重新合并all.js...');
task.js.common(pathJS, theme);
}, 100);
});
fs.watch(pathJS + '/comp', {
recursive: true
}, (eventType, filename) => {
// 定时器让多文件同时变更只会只会执行一次合并
clearTimeout(timerUIJS);
console.log(filename + '发生了' + eventType + '变化');
timerUIJS = setTimeout(() => {
console.log('重新合并all.js...');
task.js.common(pathJS, theme);
}, 100);
});
});
setTimeout(function () {
console.log(`LuLu UI ${args.join()}主题静态资源监控中...`);
}, 200);
const mimetype = {
'css': 'text/css',
'gif': 'image/gif',
'html': 'text/html',
'ico': 'image/x-icon',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'webp': 'image/webp',
'js': 'text/javascript',
'json': 'application/json',
'pdf': 'application/pdf',
'png': 'image/png',
'svg': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
'woff': 'application/font-woff',
'woff2': 'application/font-woff2',
'ttf': 'application/x-font-ttf',
'eot': 'application/vnd.ms-fontobject',
'txt': 'text/plain',
'wav': 'audio/x-wav',
'mp3': 'audio/mpeg3',
'mp4': 'video/mp4',
'xml': 'text/xml'
};
// 创建server
const server = http.createServer(function (request, response) {
var pathname = url.parse(request.url).pathname;
var realPath = path.join('./', pathname);
var ext = path.extname(realPath);
if (!ext) {
realPath = path.join(realPath, 'index.html');
ext = path.extname(realPath);
}
ext = ext ? ext.slice(1) : 'unknown';
let href = url.parse(request.url).href;
if (ext != 'unknown' && /\./.test(href) == false && /[a-z]$/i.test(href)) {
response.writeHead(302, {
'Location': href + '/'
});
response.end();
return;
}
fs.exists(realPath, function (exists) {
if (!exists) {
response.writeHead(404, {
'Content-Type': 'text/plain'
});
response.write('This request URL ' + pathname + ' was not found on this server.');
response.end();
} else {
// gtimg的js/css地址变成本地
if (ext == 'html' || ext == 'css') {
let theme = 'pure';
if (/edge/.test(realPath)) {
theme = 'edge';
} else if (/hope/.test(realPath)) {
theme = 'hope';
}
fs.readFile(realPath, 'utf8', function (err, data) {
if (err) {
response.writeHead(500, {
'Content-Type': 'text/plain'
});
response.end(err);
} else {
let contentType = mimetype[ext];
response.writeHead(200, {
'Content-Type': contentType
});
let regUrl = new RegExp('https:\\/\\/qidian.gtimg.com\\/lulu\\/' + theme, 'g');
if (ext == 'html') {
data = data.replace(regUrl, '../../theme/' + theme).replaceAll('https://unpkg.com/lu2/', '../../');
} else if (ext == 'css' && theme == 'hope') {
data = data.replace(regUrl, '..');
}
response.write(data, 'utf8');
response.end();
}
});
return;
}
fs.readFile(realPath, 'binary', function (err, file) {
if (err) {
response.writeHead(500, {
'Content-Type': 'text/plain'
});
response.end(err);
} else {
var contentType = mimetype[ext] || 'text/plain';
response.writeHead(200, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*'
});
response.write(file, 'binary');
response.end();
}
});
}
});
});
// 设置监听端口
let port = '10086';
let funStartPage = function (theme) {
let startPage = '/about.use.html';
if (theme == 'hope') {
startPage = '/about.html';
}
return theme + startPage;
}
server.listen(port, '127.0.0.1', function () {
setTimeout(function () {
console.log('文档访问服务已启动,地址:\nhttp://localhost:10086/docs/' + funStartPage(args[0]));
if (args.length > 1) {
console.log('http://localhost:10086/docs/' + funStartPage(args[1]));
}
if (args.length > 2) {
console.log('http://localhost:10086/docs/' + funStartPage(args[2]));
}
}, 200);
});