UNPKG

jspacker

Version:

支持commonjs类模块化开发项目的静态打包

296 lines (275 loc) 10.5 kB
/** * todo:支持requireJS * todo:支持OzJS * * @date 12-12-17 * @describe: 完成对 commonJS 类模块的合并压缩功能 * @author: KnightWu * @version: 0.0.1 */ var fs = require('fs'); var path = require('path'); var uglify = require('uglify-js'); var resolvepath = require('./resolvepath'); require("consoleplusplus"); console.disableTimestamp(); var crypto = require('crypto'); var shell = require('shelljs'); var configs = null; var scriptBasePath, businessOutput; const baseModuleFile = path.resolve(__dirname, './Module1.1.1'); var unicode, md5, unicodeMd5, defineMd5; var setMd5 = function () { unicode = '' + Date.now(); md5 = crypto.createHash('md5'); unicodeMd5 = md5.update(unicode) .digest('hex') .substring(0, 6); defineMd5 = 'define' + unicodeMd5; }; /** * 获取压缩混淆后的代码 * @param ast 压缩前的语法树 * @return {String} 结果代码 */ var getCompressedCode = function (ast) { // compressor needs figure_out_scope too ast.figure_out_scope(); var compressor = uglify.Compressor({warnings: false}); ast = ast.transform(compressor); ast.figure_out_scope(); ast.compute_char_frequency(); ast.mangle_names(); // get Ugly code back :) return ast.print_to_string(); }; /** * 统一js代码中出现的模块的名字,使用当前模块绝对路径截取根路径作为 moduleID * @param currentModulePath 当前模块路径 * @return {String} 计算后的路径 */ var uniteModuleId = function (currentModulePath) { return currentModulePath .replace(scriptBasePath, '') .replace(/\\/g, '/') .replace(/.js$/g, ''); }; var dependencies = {}; /** * 记录本模块依赖关系 * @param moduleId 当前模块id * @param currentModulePath 当前模块路径 * @param foreignModuleRealPath 依赖模块路径 */ var recodeDependencies = function (moduleId, currentModulePath, foreignModuleRealPath) { if (!dependencies[currentModulePath][foreignModuleRealPath]) { dependencies[currentModulePath].push({ basePath: path.dirname(currentModulePath), moduleId: moduleId, fullPath: foreignModuleRealPath }); dependencies[currentModulePath][foreignModuleRealPath] = true; } }; /** * 遍历,处理 define 的第一个参数 * 如果有一个或两个参数, * define(function(require, exports, module){ ... }); * define([...],function(require, exports, module){ ... }); * 转成 * define(moduleID[, [...]],function(require, exports, module){ ... }) * * 如果 define 有三个参数 * define(currentModulePath,[...], function(require, exports, module){ ... }); * 则替换第一个参数的id 为 moduleID * define(moduleID, [...], function(require, exports, module){ ... }); * * @param node 是define调用的节点 * @param currentModulePath 代码文件地址 * @return node 返回处理过moduleID的define节点 */ var uniteDefine = function (node, currentModulePath) { var moduleID = new uglify.AST_String({ "value": uniteModuleId(currentModulePath) }); if (node.args.length === 3) { node.args[0] = moduleID; } else { node.args.unshift(moduleID); } node.expression.name = defineMd5; return node; }; /** * 处理require调用中的相对路径为define中的绝对路径 * @param node * @param currentModulePath * @returns {*} */ var uniteRequire = function (node, currentModulePath) { var foreignModuleRealPath = resolvepath(path.dirname(currentModulePath), node.args[0].value, scriptBasePath); //获取依赖模块的绝对地址 recodeDependencies(node.args[0].value, currentModulePath, foreignModuleRealPath); node.args[0].value = uniteModuleId(foreignModuleRealPath); return node; }; /** * 处理此路径处的模块的代码,统一其define的模块id名称,统一require模块路径,并记录依赖的模块路径 * @param moduleRealPath 模块的绝对路径 * @returns {*} 返回一个uglify的语法树对象 */ var handleModule = function (moduleRealPath) { var moduleCode = fs.readFileSync(moduleRealPath, 'utf-8'); var moduleAST = uglify.parse(moduleCode.toString(), { filename: moduleRealPath }); return moduleAST.transform(new uglify.TreeTransformer(null, function (node, descend) { if (node instanceof uglify.AST_Call) { var nodeName = node.expression.name; if (nodeName === 'define' || nodeName === defineMd5) { return uniteDefine(node, moduleRealPath); } else if (nodeName === 'require') { if (node.args[0].value.indexOf('m.css') === -1) { return uniteRequire(node, moduleRealPath); } else { return uglify.parse(''); } } } if (node instanceof uglify.AST_Symbol && node.name === "define") { node.name = defineMd5; return node; } })); }; var allModule = {}; /** * 遍历所有的模块 * @param modulePath 模块路径 * @param moduleId 模块ID * @param pre 递归前的操作(进栈前) * @param after 递归后的操作(出栈操作) */ exports.walkAllModules = function walkAll(modulePath, moduleId, pre, after) { try { var moduleRealPath = resolvepath(modulePath, moduleId, scriptBasePath); var moduleDependence = dependencies[moduleRealPath] = []; //重置依赖关系数组 var result = pre(moduleRealPath, moduleId); for (var index = 0, length = moduleDependence.length; index < length; index++) { if (!allModule[moduleDependence[index].fullPath]) { //记录依赖 allModule[moduleDependence[index].fullPath] = true; walkAll(moduleDependence[index].basePath, moduleDependence[index].moduleId, pre, after); } } if (!!configs.extractTo) { var extractToBase = path.resolve(scriptBasePath, './' + configs.extractTo); var fileExtractTo = path.resolve(path.dirname(moduleRealPath).replace(scriptBasePath, extractToBase)); var targetTemplateBase = path.join(extractToBase, './' + configs.paths.template); var sourceTemplateBase = path.join(scriptBasePath, './' + configs.paths.template); shell.mkdir('-p', targetTemplateBase); if (moduleRealPath.indexOf(path.join(scriptBasePath, './output')) === 0) { var templateName = path.basename(moduleRealPath, '.js'); var targetTemplatePath = path.join(sourceTemplateBase, './' + templateName); shell.cp('-Rf', targetTemplatePath, targetTemplateBase); console.info('#yellow{[extract template]}', templateName); } else { shell.mkdir('-p', fileExtractTo); if (fs.existsSync(moduleRealPath)) { shell.cp('-Rf', moduleRealPath, fileExtractTo); } else { console.error('#red{[file not exist]}:' + moduleRealPath + '\n'); } console.info('#yellow{[extract file]}: ', moduleRealPath.replace(scriptBasePath, '')); } } else { console.log(moduleRealPath.replace(scriptBasePath, '')); } after(result, moduleId); } catch (err) { console.error(err); } }; var combinedCode = '', // 合并的代码 compressedCode = ''; //合并并压缩的代码 /** * 初始化commonJS核心模块 * 静态解析,define require exports module声明等 */ var initCommonJSModuleCore = function () { var moduleImp = fs.readFileSync(baseModuleFile + '.js', 'utf-8').toString() .replace(/window\.define/g, 'window.define' + unicodeMd5) .replace(/data-mainentry/g, 'data-mainentry_' + unicodeMd5); combinedCode += moduleImp; compressedCode += getCompressedCode( uglify.parse( moduleImp, {filename: baseModuleFile} ) ); }; /** * 将处理过的代码缓存,最终输出到指定的文件夹 * @param result * @param moduleId * @param mainModule */ var writeUglifyCode = function (result, moduleId, mainModule) { var moduleCode = result.print_to_string({ "indent_start": 0, "indent_level": 4, "quote_keys": true, "space_colon": true, "ascii_only": false, "inline_script": false, "width": 80, "max_line_len": 32000, "beautify": true, "source_map": null, "bracketize": false, "comments": true, "semicolons": true }); combinedCode += moduleCode; // 这里再进行一遍parse,是修复一个ufligy的奇怪错误,如果直接使用result,可能导致 js 的label标签压缩时候丢失,造成错误 // Use:for(){ continue Use}, 压缩后会变成 for(){ continue Use},正确的应该是 f:for(){continue f}; compressedCode += getCompressedCode(uglify.parse(moduleCode)) + '\n'; if (moduleId === mainModule) { fs.writeFileSync(businessOutput + '.js', combinedCode); fs.writeFileSync(businessOutput + '-min.js', compressedCode); } }; /** * * 打包所有主模块相关的js模块 * @param mainModule 主模块 * @param output 输出路径 * @param moduleName 输出名称 * @param rootPath 根路径(用来缩短路径) * @param config 配置对象或配置文件地址 */ exports.pack = function (mainModule, output, moduleName, rootPath, config) { setMd5(); combinedCode = ''; // 合并的代码 compressedCode = ''; //合并并压缩的代码 allModule = {}; configs = resolvepath.initConfigs(config); scriptBasePath = path.resolve(__dirname, rootPath || process.cwd());//计算项目根路径,不存在,直接使用当前执行的位置 businessOutput = path.resolve(__dirname, output) + '/' + moduleName; //创建dist目录 if (output && !fs.existsSync(output)) { fs.mkdirSync(output); } initCommonJSModuleCore(); console.info("#green{[concat files]}:"); exports.walkAllModules( __dirname, mainModule, function (moduleRealPath, moduleId) { return handleModule(moduleRealPath); }, function (result, moduleId) { writeUglifyCode(result, moduleId, mainModule); } ); console.info("#green{[Compress done.]}"); return unicodeMd5; };