├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── bin └── jdf ├── doc ├── a_tool_api.md ├── a_tool_build.md ├── a_tool_command.md ├── a_tool_config.md ├── a_tool_csssprite.md ├── a_tool_develop.md ├── a_tool_format.md ├── a_tool_lint.md ├── a_tool_server.md ├── core_css_optimize.md ├── core_plugin.md ├── core_smarty.md ├── core_tpl.md ├── core_vm.md ├── core_widget.md └── core_widgetoutputname.md ├── index.js ├── lib ├── VFS │ ├── VirtualFile.js │ ├── VirtualFileSystem.js │ └── fileType.js ├── base64.js ├── build.js ├── buildCss.js ├── buildES6.js ├── buildHTML.js ├── buildHTMLDeep.js ├── buildOutputWidget.js ├── buildWidget.js ├── compresser │ ├── compress.js │ ├── compressScheduler.js │ ├── compressWorker.js │ ├── minifyCss.js │ ├── minifyHtml.js │ ├── minifyImage.js │ └── minifyJs.js ├── concat.js ├── config.js ├── cssSprite.js ├── fileFormat.js ├── fileLint.js ├── htmlAst │ ├── index.js │ ├── nodeHandler.js │ └── walk.js ├── install │ ├── componentsData.js │ └── index.js ├── jdf.js ├── jsAst │ ├── index.js │ ├── seajsReplace.js │ └── typeCheck.js ├── output.js ├── pluginCore │ └── index.js ├── server │ ├── browserSyncServer.js │ ├── genPort.js │ ├── injector │ │ ├── dumpSeajsCombo.js │ │ └── index.js │ ├── middlewareLocal.js │ ├── middlewareVFS │ │ ├── envConfig.js │ │ ├── index.js │ │ ├── middlewareVFS.js │ │ ├── res.404.js │ │ ├── res.comboContent.js │ │ ├── res.dirView │ │ │ ├── dir.css.js │ │ │ ├── dir.html.js │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── index.js │ │ │ └── res.dirView.js │ │ ├── res.file.js │ │ ├── tpl.footer.js │ │ └── view.js │ ├── mime.js │ └── openurl.js ├── urlReplace.js ├── vm.js └── widget.js ├── package.json ├── template ├── config.json ├── css │ └── style.scss └── html │ └── index.html └── test ├── buildOutputWidget.js ├── buildWidget.js ├── buildcss.js ├── config ├── config.json └── index.js ├── index.js ├── urlReplace ├── comboUrlPath │ ├── case01.html │ └── result01.html └── index.js └── vfs ├── files ├── QR.jpg ├── css.css ├── doc.docx ├── es6.js ├── importless.less ├── js.js ├── less.less ├── sass.scss └── text.txt └── virtual-file.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .svn 4 | .idea 5 | .DS_Store 6 | .vscode 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .svn 4 | .DS_Store 5 | test 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.4.5" 4 | - "5" 5 | - "6" 6 | - "8" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/jdfx.png?downloads=true)](https://nodei.co/npm/jdfx/) 2 | 3 | [![NPM version](https://badge.fury.io/js/jdfx.png)](http://badge.fury.io/js/jdfx) [![Build Status](https://travis-ci.org/jdf2e/jdf.svg?branch=master)](https://travis-ci.org/jdf2e/jdf) 4 | 5 | ## 关于JDFX 6 | 7 | JDFX是京东前端团队自主研发的一款自动化构建工具,目的是合理、快速和高效的解决前端开发中的工程和项目问题,核心集成了本地调试、本地构建、远程布署、代码生成等一系列开发命令工具。 8 | 9 | ## 安装使用 10 | 11 | * [nodejs@4.4.5到最新LTS版本可用](http://nodejs.org/) 12 | ``` 13 | $ npm install jdfx -g 14 | ``` 15 | * 执行`jdf -V`,测试是否安装成功(注意是大写的`V`) 16 | 17 | * [快速开始](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_develop.md) 18 | 19 | * [配置config.json](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_config.md) 20 | 21 | ## 更新日志 22 | 23 | [完整日志](https://github.com/jdf2e/jdf/blob/master/CHANGELOG.md) 24 | 25 | ### 3.4.13 / 2020/11/20 18:34:00 26 | * [fix] 修改node-sass版本,修复node-sass更新到5.0.0后无法在node 8安装的问题。 27 | 28 | ## 说明文档 29 | * [示例安装](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_develop.md) 30 | * [命令文档](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_command.md) 31 | * [配置文档](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_config.md) 32 | * [api调用文档](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_api.md) 33 | * [css优化策略](https://github.com/jdf2e/jdf/blob/master/doc/core_css_optimize.md) 34 | * [css雪碧图](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_csssprite.md) 35 | * [smarty模版](https://github.com/jdf2e/jdf/blob/master/doc/core_smarty.md) 36 | * [tpl模版](https://github.com/jdf2e/jdf/blob/master/doc/core_tpl.md) 37 | * [vm模版](https://github.com/jdf2e/jdf/blob/master/doc/core_vm.md) 38 | * [widget说明](https://github.com/jdf2e/jdf/blob/master/doc/core_widget.md) 39 | * [widgetOutputName标签](https://github.com/jdf2e/jdf/blob/master/doc/core_widgetoutputname.md) 40 | * [插件模块](https://github.com/jdf2e/jdf/blob/master/doc/core_plugin.md) 41 | 42 | ## 使用攻略 43 | * [文件路径拼写说明](https://github.com/jdf2e/jdf/issues/6) 44 | * [移动端页面开发](https://github.com/jdf2e/jdf/issues/7) 45 | 46 | ## 功能介绍 47 | 48 | #### 跨平台 49 | * 完美支持windows、mac、linux三大系统 50 | 51 | #### 项目构建 52 | * 生成标准化的项目文件夹 53 | * 支持本地联调,本地编译,测试预览等开发流程 54 | * 每个项目拥有独立的配置文件,按选项统一编译 55 | 56 | #### 模块开发 57 | * 可快速方便的对模块进行创建,引用,预览,安装和发布 58 | * 通过积累,可形成完全符合自己业务的模块云服务 59 | * 支持将vm和smarty模版编译为html 60 | * 支持将sass和less编译为css 61 | * 支持velocity语法 62 | * 支持ES6(需要将js文件后缀改为`.babel`) 63 | 64 | #### 项目输出 65 | * 自动将页面中的js、css引用转换成combo请求格式 66 | * 自动压缩优化js、css、图片文件 67 | * 默认给所有静态资源添加CDN域名 68 | * 支持cmd规范,自动提取文件id和dependencies,压缩时保留require关键字 69 | * 自动生成css精灵图,并更新background-position属性值 70 | * 自动生成base64编码 71 | * 自动给css样式添加相应的浏览器前缀 72 | * 支持图片生成webp格式,并更新相关css图片链接 73 | * 压缩css、js、图片文件,并且可根据当前项目中的文件数量自动决定是否启用多线程进行压缩,当前的数量阀值是`200` 74 | * 自动给js,css文件的内容头部添加时间戳,方便查看 75 | * 文件编码统一输出为utf8 76 | 77 | #### 项目联调 78 | * 一键上传文件到测试服务器,方便其他同学开发预览 79 | 80 | #### 本地服务 81 | * 支持开启本地服务器,方便调试 82 | * 支持本地静态文件预览,内置本地开发调试服务器,以及当前目录浏览 83 | * 支持实时监听文件,文件被修改时会自动编译成css,并刷新浏览器 84 | * 内置browserSync 85 | * [详细文档](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_build.md) 86 | 87 | #### 辅助工具 88 | * 支持html/js/css文件格式化 89 | * 支持html/js/css代码压缩 90 | * 支持html/js/css文件lint,代码质量检查 91 | 92 | ## 集成工具 93 | 94 | * [在本地任意目录开启一个server静态服务器](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_server.md) 95 | * [html/js/css文件lint代码质量检查](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_lint.md) 96 | * [html/js/css文件格式化](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_format.md) 97 | * [csssprite图片合并](https://github.com/jdf2e/jdf/blob/master/doc/a_tool_csssprite.md) 98 | 99 | ## widget组件 100 | 101 | * 详情请参考[widget文档](https://github.com/jdf2e/jdf/blob/master/doc/core_widget.md) 102 | * [vm模版文档](https://github.com/jdf2e/jdf/blob/master/doc/core_vm.md) 103 | * [tpl模版文档](https://github.com/jdf2e/jdf/blob/master/doc/core_tpl.md) 104 | * [smarty模版文档](https://github.com/jdf2e/jdf/blob/master/doc/core_smarty.md) 105 | 106 | ## 编译器插件 107 | * [Sublime Text2 插件](https://sublime.wbond.net/packages/Jdf%20-%20Tool) 108 | 109 | -------------------------------------------------------------------------------- /bin/jdf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.stdout.isTTY = true; 3 | process.stderr.isTTY = true; 4 | require('../index.js').init(process.argv); -------------------------------------------------------------------------------- /doc/a_tool_api.md: -------------------------------------------------------------------------------- 1 | # api文档 2 | 3 | ## 安装使用 4 | 5 | ```javascript 6 | npm install jdfx 7 | ``` 8 | 9 | ```javascript 10 | const jdfx = require('jdfx'); 11 | 12 | jdfx.build(port => { 13 | console.log(port); 14 | }); 15 | ``` 16 | 17 | ## 方法说明 18 | 19 | ### jdf.setConfig([options]) 20 | 21 | 设置jdf的全局运行参数 22 | 23 | ```javascript 24 | const jdfx = require('jdfx'); 25 | 26 | jdfx.setConfig({ 27 | projectPath: '/Users/chenxiaochun/Documents/MyProject/jdf-test' 28 | }) 29 | ``` 30 | 31 | * `options`,全局参数设置 32 | * `projectPath`,指定项目运行的绝对路径,默认为当前目录 33 | 34 | ### jdf.server([options, callback]) 35 | 36 | ```javascript 37 | const jdfx = require('jdfx'); 38 | 39 | jdfx.server(() => { 40 | console.log('server is ok'); 41 | }); 42 | ``` 43 | 44 | * `options`,当前服务配置 45 | * `open`,是否自动在浏览器中打开当前服务,默认为`false` 46 | * `watch`,是否实时监听当前项目的文件变动,默认为`false` 47 | * `callback`,服务启动之后的回调函数 48 | 49 | ### jdf.build([options, callback]) 50 | 51 | ```javascript 52 | const jdfx = require('jdfx'); 53 | 54 | jdfx.build(port => { 55 | console.log(port); 56 | }); 57 | ``` 58 | 59 | * `options`,当前服务配置 60 | * `open`,是否自动在浏览器中打开当前服务,默认为`false` 61 | * `callback`,服务启动之后的回调函数,当前参数为服务的端口号 62 | 63 | ### jdf.output([dir, options]) 64 | 65 | ```javascript 66 | const jdfx = require('jdfx'); 67 | 68 | jdfx.output(); 69 | ``` 70 | 71 | * `dir`,指定需要单独输出的目录,类型为数组 72 | * `options`,输出时的类型配置 73 | * `debug`,输出时不压缩文件,不对html文件中引用的资源进行combo,默认为`false` 74 | * `plain`,只对项目进行编译,不压缩文件,不对html文件中引用的资源进行combo,默认为`false` 75 | 76 | ### jdf.upload([dir, options]) 77 | 78 | 上传的方法内部会默认调用`jdf.output()`方法,因此,不需要在上传之前单独调用输出方法 79 | 80 | ```javascript 81 | const jdfx = require('jdfx'); 82 | 83 | jdfx.upload(); 84 | ``` 85 | 86 | * `dir`,想要单独上传的文件目录,类型为数组 87 | * `options`,上传时的配置参数 88 | * `type`,指定上传方式:`ftp|scp|http`,默认为`http` 89 | * `debug`,上传时不压缩文件,不对html文件中引用的资源进行combo,默认为`false` 90 | * `plain`,只对项目进行编译,不压缩文件,不对html文件中引用的资源进行combo,默认为`false` 91 | * `preview`,上传模版文件到服务器,默认为`false` 92 | 93 | ### jdf.clean() 94 | 95 | 清除当前项目的jdf缓存文件 96 | 97 | ### jdf.exit() 98 | 99 | 退出当前项目的jdf服务 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /doc/a_tool_build.md: -------------------------------------------------------------------------------- 1 | # jdf build 2 | 3 | ## 简介 4 | `jdf build | jdf b`命令主要用于编译jdf项目并开启[本地server](a_tool_server.md)来调试项目。 5 | 6 | ## 特点 7 | * 自动刷新 8 | 保存文件自动编译并刷新浏览器,无需安装任何插件; 9 | * 多终端同步 10 | 多终端同步响应鼠标键盘动作,实时查看多终端对同一操作的效果; 11 | * 导航 12 | 浏览器中显示项目文件夹列表,访问目标页面无需在浏览器输出访问路径; 13 | * 移动端调试 14 | jdf集成weinre工具,通过weinre,可以调试移动设备中页面; 15 | ## 命令参数 16 | 17 | * `--open` 或 `-o`,在开启本地服务的同时,自动在浏览器中打开根目录文件列表页面 18 | * `--help` 或 `-h`,查看jdf b帮助 19 | 20 | ## 控制台信息 21 | 22 | 编译成功后,控制台中会打印如下信息: 23 | 24 | [JDFX] Access URLs: 25 | -------------------------------------- 26 | Local: http://localhost:80 27 | External: http://192.168.191.1:80 28 | -------------------------------------- 29 | UI: http://localhost:3001 30 | UI External: http://192.168.191.1:3001 31 | -------------------------------------- 32 | 33 | * `Local`,本地服务器地址 34 | * `External`,同网段内其他机器访问地址,用于移动端访问 35 | * `UI`,jdf服务器控制面板地址,从这个入口可开启weinre,模拟网络限流等功能 36 | * `UI External`,同网段内访问服务器控制面板地址 37 | 38 | 39 | ## 使用方法 40 | 41 | ### PC端 42 | * 用`jdf init xxx`创建jdf工程后,在控制台输入`jdf b -o`后保留这个控制台运行,方便实时调试; 43 | * 在自动打开的浏览器页面中点击导航到开发页面,编辑widget等文件,查看浏览器的同步改动; 44 | * 引用mock来模拟数据生成和读取,建议[mock](https://github.com/nuysoft/Mock)。 45 | * 去除mock引用,[切换hosts](https://github.com/oldj/SwitchHosts),模拟线上环境。 46 | 47 | ### M端 48 | * 将移动设备与开发电脑连接到同一网段内; 49 | * 在控制台输入`jdf b -o`,保留这个控制台; 50 | * 用移动设备扫描浏览器弹出页面上的二维码或者在移动设备浏览器上输入`External`的网址; 51 | * 在电脑或手机上导航到开发页面,编辑widget等文件,查看移动设备和开发电脑上浏览器的页面改动; 52 | 53 | ### M端调试 54 | * 在`jdf b -o`运行时,可以通过修改本地文件实时查看M端页面改动; 55 | * 如果需要微调,则可以使用集成的weinre,如何使用请参见[weinre说明](a_tool_weinre.md); 56 | 57 | ## 常见问题 58 | * 终端报acron.js错误 59 | - 请检查js有无写错 60 | - html中的模板由` 75 | 76 | ``` 77 | 进行combo之后为: 78 | ```html 79 | 80 | ``` 81 | 82 | * 生成精灵图[cssSprite](a_tool_csssprite.md) 83 | * 生成base64编码 84 | 85 | --- 86 | 87 | * `-d`或者`--debug`,以debug的模式输出当前项目,不压缩项目中的任何文件 88 | * `-v`或者`--verbose`,将会详细输出当前项目每一个文件的编译信息。此参数特别适用于输出时卡死的情况,可以方便的查看问题出在了哪一个文件上 89 | * `-p`或者`--plain`,只编译widget、less、scss,不做任何其它处理。此模式适用于页面是由前端开发,然后需要把页面交付给后端的同学来完成剩下的工作 90 | * 支持输出指定的文件夹,例如:`jdf o js` 91 | * 支持输出指定的文件,例如:`jdf o js/a.js` 92 | * 支持简单的通配符,例如:`jdf o js/**/*.js`,将只输出`js`文件夹下所有的js文件 93 | 94 | ## jdf upload 95 | 96 | 把当前项目上传到测试服务器 97 | 98 | * 可简写为`jdf u` 99 | * 目前支持三种上传模式:ftp,sftp,http 100 | * `-d`或者`--debug`,以debug的模式输出当前项目,不压缩项目中的任何文件 101 | * `-p`或者`--plain`,只编译widget、less、scss,不做任何其它处理。此模式适用于前端同学仅仅做静态页面,然后把页面交付给后端的同学来完成剩下的工作 102 | * `-P`或者`--preview`,将当前项目上传到`previewServerDir`配置的目录之下 103 | * `-v`或者`--verbose`,将会详细输出当前项目每一个文件的编译信息。此参数特别适用于上传时卡死的情况,可以方便的查看问题出在了哪一个文件上 104 | * 支持上传指定的文件夹,例如:`jdf u js` 105 | * 支持上传指定的文件,例如:`jdf u js/a.js` 106 | * 支持简单的通配符,例如:`jdf u js/**/*.js`,将只输出`js`文件夹下所有的js文件 107 | 108 | ## jdf widget 109 | 110 | * 可简写为`jdf w` 111 | * `-s` 或者 `--smarty`,创建widget时以smarty模板形式创建 112 | * `-c xxx`或者`--create xxx`,创建一个widget 113 | * `-P xxx`或者`--preview [xxx]`,预览指定的widget,当没有指定widget时,预览全部widget 114 | * `-l xxx`或者`--list`,获取服务器上所有的widget列表 115 | * `-p xxx`或者`--publish xxx`,发布widgt到服务器上 116 | * `-i xxx`或者`--install xxx`,安装指定的widget到当前项目 117 | 118 | ## jdf lint 119 | 120 | html、css、js文件代码质量检查工具,详细用法可点击[这里](a_tool_lint.md) 121 | 122 | * 可简写为`jdf l xxx`,后面跟指定的文件夹/文件 123 | * [详细说明](a_tool_lint.md) 124 | 125 | ## jdf format 126 | 127 | html、css、js文件格式化工具,详细用法可点击[这里](a_tool_format.md) 128 | 129 | * 可简写为`jdf f xxx`,后面跟指定的文件夹/文件 130 | * [详细说明](a_tool_format.md) 131 | 132 | ## jdf compress 133 | 134 | html、css、js文件压缩工具,详细用法可点击[这里](a_tool_deploy.md) 135 | 136 | * 可简写为`jdf c xxx`,后面跟指定的文件夹/文件 137 | 138 | ## jdf clean 139 | 140 | 清理jdf缓存文件,遇到比较反常的现象时,可尝试执行此命令 141 | 142 | ## jdf -h 143 | 144 | 获取jdf的帮助信息 145 | 146 | ## jdf -V 147 | 148 | 获取jdf的当前版本号,注意是大写的`V` 149 | 150 | ## jdf参数配置文档 151 | 152 | 请参考:[jdf参数配置文档](a_tool_config.md)进行查阅。 153 | 154 | 155 | -------------------------------------------------------------------------------- /doc/a_tool_config.md: -------------------------------------------------------------------------------- 1 | # 配置文档 2 | 3 | 每一个项目的根目录都有一个独立的config.json配置文件,其详细配置如下: 4 | 5 | * `"projectPath": null` - 【常用】工程目录前缀 6 | 7 | * `"cssDir": "css"` - css文件夹名称 8 | 9 | * `"imagesDir": "css/i"` - images文件夹名称 10 | 11 | * `"jsDir": "js"` - js文件夹名称 12 | 13 | * `"htmlDir": "html"` - html文件夹名称 14 | 15 | * `"widgetDir": "widget"` - widget文件夹名称 16 | 17 | * `"outputDirName": "build"` - 输出文件夹名称 18 | 19 | * `"outputCustom": ""` - 自定义输出文件夹,以逗号分隔的字符串 20 | 21 | * `"cdn": "//misc.360buyimg.com"` - 【常用】静态cdn域名 22 | 23 | * `"serverDir": "misc.360buyimg.com"` - 上传至远端服务器文件夹的名称 24 | 25 | * `"previewServerDir": "page.jd.com"` - html文件夹上传至服务器所在的文件夹名称 26 | 27 | * `"widgetServerDir": "jdfwidget.jd.com"` - widget服务器所在的文件夹名称 28 | 29 | * `"widgetOutputName": "widget"` - 全局widgetOutputName名称 30 | 31 | * `"widgetOutputMode": 1` - 编译全局wigetOutputName模式,共三种:1: all widgets|2: white list|3: black list 32 | 33 | * `"widgetWhiteList": []` - 指定白名单,在widgetOutputMode=2时,输出这个列表内容到widget.js/widget.css中 34 | 35 | * `"widgetBlackList": []` - 指定黑名单,在widgetOutputMode=3时,排除这个列表的widget 36 | 37 | * `"widgetNesting": true` - widget嵌套功能开关,默认开启 38 | 39 | * `"localServerPort": 80` - 【常用】本地服务器端口 40 | 41 | * `"build"` 42 | * `"jsPlace": "insertBody"` - 调试时js文件位置 insertHead|insertBody 43 | 44 | * `"livereload":true` - 是否开启liveload 45 | 46 | * `"sass":true` - 是否开启sass编译 47 | 48 | * `"less":true` - 是否开启less编译 49 | 50 | * `"csslint": false` - 是否开启csslint 51 | 52 | * `"upload"` 53 | * `"type": "http"` - 默认 ftp scp http 54 | * `"host": null` - 服务器的域名或者ip 55 | * `"user": null` - 上传时使用的用户名, ftp、scp需要,http不需要 56 | * `"password": null` - 规则同上 57 | * `"port": null` - 传输端口,ftp默认21,scp默认22,http默认3000 58 | * `"rootPrefix": "/var/www/html/"` - scp时传输的目录前缀,用来确认上传文件最终的地址,一个文件最终的地址会是rootPrefix + serverDir + projectPath + filePath,你可以根据自身server的配置来修改这个值 59 | 60 | * `"output"` 61 | * `"cssImagesUrlReplace": true` - css中图片url加cdn替换 62 | 63 | * `"linkReplace": true` - 给link.href添加cdn前缀,v3.3.0版本新增,之前由`jsUrlReplace`参数控制link.href添加cdn 64 | 65 | * `"jsUrlReplace": false` - js文件的id和dependences是否添加cdn前缀 66 | 67 | * `"jsPlace": "insertBody"` - 编译后js文件位置 insertHead|insertBody 68 | 69 | * `"cssCombo": true` - 【常用】css进行combo 70 | 71 | * `"jsCombo": true` - 【常用】js进行combo todo 72 | 73 | * `"hasBanner": true` - 是否给js文件,css文件添加banner时间戳 74 | 75 | * `"compressJs":true` - 【常用】是否开启压缩js文件 76 | 77 | * `"compressCss":true` - 【常用】是否开启压缩css文件 78 | 79 | * `"compressImage":true` - 【常用】是否开启压缩图片 80 | 81 | * `"cssSprite":true` - 是否开启css sprite功能 82 | 83 | * `"cssSpriteMode": 1` - 0: 将所有css文件中的背景图合并成一张sprite图片,1: 将每一个widget中的背景图分别合并成一张图片 84 | 85 | * `"cssSpriteMargin": 10` - css sprite图片之间的间距 86 | 87 | * `"cssSpriteDirection": vertical` - vertical:垂直合并,horizontal:水平合并 88 | 89 | * `"cssAutoPrefixer": true` - 【常用】是否自动删除过时的浏览器css前缀,如`-webkit-box-orient: vertical;` 90 | 91 | * `"browserslist": ["last 2 version", "> 0.2%", "ie > 7"]` - 【常用】配置autoprefixer的browserslist。 92 | 93 | * `"base64": false` - 是否对图片进行base64编码 94 | 95 | * `"webp":false` - 是否生成对应的webp图片 96 | 97 | * `"excludeFiles": ""` - 【常用】想要直接忽略的文件/文件夹,以逗号分隔的字符串:"test,build" 98 | 99 | * `"babel"` - 默认只启用基本转义(3.4.10及之前preset-es2015,之后为preset-env),您也可以在项目根目录下新建一个`.babelrc`文件独立配置 100 | * `"presets": []` 101 | * `"plugins": []` 102 | 103 | 104 | -------------------------------------------------------------------------------- /doc/a_tool_csssprite.md: -------------------------------------------------------------------------------- 1 | # csssprite图片合并 2 | 3 | ## 使用说明 4 | 5 | ### 默认单位为px 6 | 非常简单,只需要在css文件中对要合并的图片路径增加?__sprite后缀即可,比如 7 | ``` css 8 | .csssprite .abtest_huafei s { 9 | background:url(i/icon_01.png?__sprite) no-repeat; 10 | } 11 | .csssprite .abtest_lvxing s { 12 | background:url(i/icon_03.png?__sprite) no-repeat 6px 0px; 13 | } 14 | .csssprite .abtest_caipiao s { 15 | background:url(i/icon_05.png?__sprite) no-repeat 5px 0px; 16 | } 17 | ``` 18 | 19 | 执行jdf output,后台会进行css sprite编译操作后 20 | ``` css 21 | .csssprite .abtest_huafei s { 22 | background:url(i/csssprite.png) no-repeat; 23 | background-position:0 0 24 | } 25 | .csssprite .abtest_lvxing s { 26 | background:url(i/csssprite.png) no-repeat 6px 0; 27 | background-position:6px -39px 28 | } 29 | .csssprite .abtest_caipiao s { 30 | background:url(i/csssprite.png) no-repeat 5px 0; 31 | background-position:5px -78px 32 | } 33 | ``` 34 | 其中`icon_01.png`,`icon_03.png`,`icon_05.png`小图片被合成为`csssprite.png`,其中csssprite为当前css文件的文件名 35 | 36 | ### 当css单位为rem时 37 | 38 | 在background中写上px到rem的转换比例 39 | ``` css 40 | html { 41 | font-size: 20px; 42 | } 43 | .icon1, .icon2{ 44 | width: 1.8rem; 45 | height: 1.8rem; 46 | margin: 10px; 47 | background: url(i/icon7.png?__sprite__rem20) no-repeat; 48 | border: 1px solid black; 49 | } 50 | .icon2{ 51 | background: url(i/icon8.png?__sprite__rem20) no-repeat; 52 | } 53 | ``` 54 | 转换之后: 55 | ```css 56 | html { 57 | font-size: 20px 58 | } 59 | .icon1, .icon2 { 60 | width: 1.8rem; 61 | height: 1.8rem; 62 | margin: 10px; 63 | background: url(/i/w2.png) no-repeat; 64 | background-position: 0 0; 65 | background-size: 1.8rem 4.6rem; 66 | border: 1px solid #000 67 | } 68 | .icon2 { 69 | background: url(i/w2.png) no-repeat; 70 | background-position: 0 -2.3rem; 71 | background-size: 1.8rem 4.6rem 72 | } 73 | ``` 74 | 75 | ## 切图说明 76 | 把psd中图片所有icon类小图切换,在css中设置好background-position,在相对应图片后面增加?__sprite后缀 77 | 78 | ## 配置说明 79 | * 默认为开启状态,可以通过config.json的output.csssprite键值设置为false进行关闭 80 | * 图片之间上下间距,可以通过config.json的output.cssspriteMargin键值设置 81 | * 合并文件,通过config.json的outout.cssSpriteMode 0|1 设置把整个项目的雪碧图合并到一起,还是以widget为单位合并到一起,默认是1。 82 | 83 | ## 特性说明 84 | * 支持的图片格式:png,jpg,png输出png24格式,IE6的png24图片需要单独处理 85 | * 支持no-repeat,background-position可自由设置 86 | * 后续支持repeat-x,repeat-y 87 | 88 | ## 原理解析 89 | * 分析css文件内容,取出带有?__sprite的图片路径,同时对此background的backgroud-repeat、background-position进行记录 90 | * 取出所有图片,依靠backgroud-repeat、background-position进行图片合并,并生成合并的新图片 91 | * 把css文件所有sprite图片路径替换成合并的新图片路径 92 | 93 | ## 解析css 94 | * 利用正则实现一个简单的css语法解析器,可把css内容解析为 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
属性 说明
content 图片内容
url 如:i/icon.png 图片url
item 如:background: url(i/icon.png?__sprite) no-repeat 图片background
repeat null | repeat-x | repeat-y 重复
width number 图片的宽度
height number 图片的高度
105 | -------------------------------------------------------------------------------- /doc/a_tool_develop.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | ## 欢迎使用jdf。 3 | jdf是一个前端工程框架,通过提供若干命令行,jdf可以创建工程,编译,在线调试,输出压缩代码等。 4 | ### 安装jdf 5 | ``` 6 | npm install -g jdfx 7 | ``` 8 | 安装完成后运行`jdf -h`,查看jdf所有命令。 9 | 10 | #### 注意 11 | 实际安装中,windows可能遇到第一次安装时,node-sass依赖安装不成功的问题,这个时候卸载jdfx,`npm remove -g jdfx`, 然后再重装`npm install -g jdfx`就ok了,再次安装的速度会比第一次安装快很多。 12 | 13 | ### 新建工程 14 | * 使用`jdf init xxx`来创建一个符合jdf规范目录的工程,比如创建一个名为helloworld的工程: 15 | 16 | ``` 17 | jdf init helloworld 18 | ``` 19 | 20 | * `cd helloworld`进入工程根目录,在工程根目录下运行如下命令创建一个widget: 21 | 22 | ``` 23 | ~/jd/web$ cd helloworld/ 24 | ~/jd/web/helloworld$ jdf widget --create myWidget 25 | ``` 26 | 27 | 根据提示一直输入`y`,创建myWidget包含的文件,默认提供模板vm,脚本js,样式scss,数据json,这些都是可选的,更多关于widget的信息请参见[widget](core_widget.md). 28 | 下面的命令在没有指明的情况下,都在helloworld目录下运行。 29 | * 创建widget后,得到的目录结构如下: 30 | ``` 31 | helloworld/ 32 | ├── config.json 33 | ├── css 34 | │   └── i 35 | ├── html 36 | │   └── index.html 37 | ├── js 38 | └── widget 39 | └── myWidget 40 | ├── component.json 41 | ├── myWidget.js 42 | ├── myWidget.json 43 | ├── myWidget.scss 44 | └── myWidget.vm 45 | ``` 46 | 47 | 这样,一个jdf工程的创建就完成了。 48 | 49 | ### 进入开发阶段 50 | #### 引用widget到html页面 51 | * 新建html/myPage.html文件 52 | * 将myWidget引入myPage.html 53 | 54 | ``` 55 | 56 | {%widget name="myWidget" %} 57 | 58 | ``` 59 | 60 | 利用`{%widget %}`标签引用widget,jdf在执行编译和输出命令时会将widget里的信息编译进来。当然,如果只需要引用widget的css和vm文件,那么可以加`type`属性: 61 | 62 | ``` 63 | {%widget name="myWidget" type="css, vm" %} 64 | ``` 65 | 66 | #### 编写widget内容 67 | * 在`myWidget.vm`中输入: 68 | 69 | ``` 70 |

welcome FEer

71 |

hope you enjoy jdf!

72 | ``` 73 | 74 | * 在`myWidget.scss`中输入: 75 | 76 | ``` 77 | .p1 { 78 | font-size: 18px; 79 | span { 80 | color: blue; 81 | } 82 | } 83 | .p2 { 84 | color: red; 85 | } 86 | ``` 87 | 88 | #### 启动开发调试模式 89 | 欢迎进入欢快的开发阶段! 90 | 91 | * 运行`jdf build -o`,编译工程并自动打开浏览器,假设打开的网址为: 92 | `http://192.168.191.1:8080`, 93 | 可以通过点击页面的文件路径一直跳转到: 94 | `http://192.168.191.1:8080/html/myPage.html` 95 | 96 | * myPage.html页面显示效果 97 | 98 | ![myPage](http://img10.360buyimg.com/uba/jfs/t3859/296/484880740/2242/f0dafc20/58527f94N53cb0cbc.jpg) 99 | 100 | * 随意改动html,vm,js,scss文件,保存,可以在浏览器中看到改动同步刷新了。 101 | 102 | 你的项目开发进度良好! 103 | 104 | 105 | #### 输出项目 106 | 项目开发完以后,需要将编译后的文件放到线上服务器或者CDN,因此需要输出项目内容。 107 | 108 | 执行`jdf output`,jdf默认会将项目输出到`build`目录中,如果在config.json配置了`projectPath`,那么就会输出到`build/projectPath`中,例如 109 | 110 | ``` 111 | projectPath: 'helloworld/1.0.0' 112 | ``` 113 | 114 | 那么输出的目录结构为: 115 | 116 | ``` 117 | build/ 118 | └── helloworld 119 | └── 1.0.0 120 | ├── html 121 | │   └── index.html 122 | │ └── myPage.html 123 | └── widget 124 | └── myWidget 125 | ├── component.json 126 | ├── myWidget.css // scss -> css 127 | ├── myWidget.js 128 | └── myWidget.json 129 | ``` 130 | 131 | 恭喜你的项目开发完毕,让我们启动服务器试试输出的项目能不能工作吧! 132 | 133 | #### 启动服务器 134 | 进入build/helloworld/1.0.0目录,运行`jdf server`命令,开启一个静态服务器来查看输出结果是否正确。 135 | 136 | ``` 137 | ~/jd/web/helloworld/build/helloworld/1.0.0$ jdf server -o 138 | ``` 139 | 140 | 由于`jdf output`会根据你在config.json文件中的配置定制输出,通过`jdf server`来查看这些配置是否会影响页面效果是很有必要的。 141 | 142 | 检查完毕,页面和预期完全一致 143 | 144 | #### 上传到测试服务器测试 145 | 代码开发完毕,后端和业务方需要查看效果,这个时候就可以把代码上传到测试服务器,让大家都能访问。 146 | 147 | jdf上传的测试服务器可以是基于HTTP、FTP、SFTP协议的服务器,在config.json中配置好服务器地址: 148 | 149 | ``` 150 | host: xx.xx.xx.93 151 | ``` 152 | 153 | 然后执行: 154 | ``` 155 | jdf upload 156 | ``` 157 | 158 | 这样就上传到测试服务器了,邀请团队的小伙伴来查看你的成果吧。 159 | 160 | ## 结语 161 | 通过上述操作,你已经掌握jdf的主要功能,可以进行完整的工程开发了。jdf还有很多特性,我们也提供了完善的说明文档,欢迎探索。 162 | 163 | -------------------------------------------------------------------------------- /doc/a_tool_format.md: -------------------------------------------------------------------------------- 1 | # html/js/css文件格式化 2 | 3 | ## 使用说明 4 | 5 | * 方法1:在当前目录中,使用 `jdf format` 或者 `jdf format ./test` 可直接格式化当前目录或者指定目录下的所有文件。 6 | * 方法2:在当前目录中,使用 `jdf format test.html` 可直接格式化指定的文件。 7 | 8 | ## 使用示例 9 | 10 | `jdf format` 可快速格式化文件中的代码格式,比如 `test.html` 的内容如下: 11 | 12 | 13 | 14 | 15 | 16 | Document 17 | 18 | 19 |
20 |

jdf format

21 |
22 | 25 | 28 | 29 | 30 | 31 | 执行 `jdf format test.html` 或者 `jdf f test.html` 命令之后,此文件的代码会被格式化成: 32 | 33 | 34 | 35 | 36 | 37 | Document 38 | 39 | 40 |
41 |

jdf format 42 |

43 |
44 | 51 | 57 | 58 | 59 | 60 | ## 注意事项 61 | 62 | * 此工具会自动**递归格式化**指定目录中所有的html、vm、tpl、css、sass、less、js文件,其它文件会自动忽略,请谨慎指定目录。 63 | * 此工具会同时格式化html文档中包含的所有html、css、js代码。 64 | * 格式化后的代码会被覆盖保存到原文件中。 -------------------------------------------------------------------------------- /doc/a_tool_lint.md: -------------------------------------------------------------------------------- 1 | # html/js/css文件lint代码质量检查 2 | 3 | ## 使用说明 4 | 5 | * 方法1:在当前目录中,使用 `jdf lint` 或者 `jdf lint ./test` 可直接检查当前目录或者指定目录下的所有文件。 6 | * 方法2:在当前目录中,使用 `jdf lint test.html` 可直接检查指定的文件。 7 | * 方法3:可直接使用 `jdf lint http://www.jd.com` 检查在线页面。 8 | 9 | ## 检查html文件 10 | 11 | `jdf lint` 可以快速检测html文件中代码的书写错误,比如 `test.html` 的内容如下: 12 | 13 |
14 |
15 | 16 | 17 | 18 | 运行 `jdf lint test.html` 命令之后,会看到以下提示信息: 19 | 20 | jdf lint: test/test.html 21 | #1 22 | >> line: 9, column: 2 23 | >> msg: the id "box" is already in use 24 | #2 25 | >> line: 10, column: 2 26 | >> msg: the "type" attribute is not double quoted 27 | 28 | 很明显,在此hmtl文件中存在两个问题: 29 | 30 | * 问题1:页面中有两个元素使用了重复的id 31 | * 问题2:`input` 元素的 `type` 属性值没有加双引号 32 | 33 | ## 检查css文件 34 | 35 | `jdf lint` 可以快速检测css的书写错误,比如 `btn.css` 内容如下: 36 | 37 | .btn{ 38 | colo: #fff 39 | border:1px solid red; 40 | } 41 | 42 | 很明显 `.btn` 样式存在两个问题 43 | 44 | * 问题1: `colo` 属性是不存在的,可能应该为 `color` 45 | * 问题2: `colo: #fff` 后和 `border:1px solid red;` 前少了一个分号 `;` 46 | 47 | 此时jdf命令行下会有如下提示,方便查找问题所在 48 | 49 | jdf csslint: There are 2 problems in lib/csslint.css 50 | 51 | #1 warning at line 2, col 2 52 | Unknown property 'colo'. 53 | colo: #fff 54 | 55 | #2 error at line 3, col 8 56 | Expected RBRACE at line 3, col 8. 57 | border:1px solid red; 58 | 59 | 注意:less/scss文件需要编译成css后才能检测,否则提示会不准确。 60 | 61 | ## 检查js文件 62 | 63 | `jdf lint` 可快速检测js代码的书写错误,比如 `test.js` 的内容如下: 64 | 65 | function test() { 66 | var a = 1 67 | } 68 | 69 | 运行 `jdf lint test.js` 命令之后,会看到以下提示信息: 70 | 71 | #1 72 | >> line: 4, column: 12 73 | >> msg: Expected ";" and instead saw "}". 74 | >> at: var a = 1 75 | #2 76 | >> line: 4, column: 7 77 | >> msg: Unused "a". 78 | >> at: var a = 1 79 | 很明显,在此js文件中有两个问题: 80 | 81 | * 问题1:在代码 `var a = 1` 结尾处没有加分号 82 | * 问题2:a变量只是被定义了,却没有被使用 83 | 84 | ## 注意事项 85 | 86 | * 此工具会自动**递归检查**指定目录中所有的html、vm、tpl、css、sass、less、js文件,其它文件会自动忽略。 87 | * 在使用 `jdf lint` 检查在线页面时,url前必须要加上http。 88 | * 默认csslint功能是关闭状态,可以通过config.json的{{build.csslint}}键值设置为true进行开启,这样在 `jdf build` 下会自动检测 89 | 90 | ## 原理浅析 91 | 92 | csslint可用于检查CSS取值和潜在问题,使用了Nicholas大神的npm模块parser-lib作为css解析器,并按照parser-lib给出的API来编写检查规则。 93 | csslint的每一个规则都是通过监听parser-lib给出的事件来进行相应的判断: 94 | 95 | * startrule为规则开始 96 | * property为找到一个属性时的事件 97 | * endrule为一个规则结束 98 | 99 | 一旦规则结束并且没有统计到任何property,则说明规则为空。 100 | -------------------------------------------------------------------------------- /doc/a_tool_server.md: -------------------------------------------------------------------------------- 1 | # jdf server 2 | 3 | ## 简介 4 | 在本地开发时,需要利用谷歌插件辅助开发或需要调试ajax,jsonp,这个时候就需要将开发文件置于服务器中。 5 | `jdf server | jdf s`命令用于开启一个静态服务器,类似[http-server](https://github.com/indexzero/http-server),可以在任何目录即使该目录下不是JDF工程开启一个静态服务器。 6 | 7 | ## 命令参数 8 | 9 | * `--open` 或 `-o`,在开启静态服务器的同时,自动在浏览器中打开当前目录文件列表页面 10 | * `--watch` 或 `-w`,监听当前目录的文件改动,并实时在浏览器中刷新改动内容 11 | * `--help` 或 `-h`,查看jdf server帮助 12 | 13 | ## 控制台信息 14 | 15 | 编译成功后,控制台中会打印如下信息: 16 | 17 | [JDFX] Access URLs: 18 | -------------------------------------- 19 | Local: http://localhost:80 20 | External: http://192.168.191.1:80 21 | -------------------------------------- 22 | UI: http://localhost:3001 23 | UI External: http://192.168.191.1:3001 24 | -------------------------------------- 25 | 26 | * `Local`,本地服务器地址 27 | * `External`,同网段内其他机器访问地址,用于移动端访问 28 | * `UI`,jdf服务器控制面板地址 29 | * `UI External`,同网段内访问服务器控制面板地址,从这个入口可开启weinre,模拟网络限流等功能 30 | 31 | ## TIPS 32 | * `jdf server`只提供静态服务和部分类型文件监听功能,不对任何文件进行编译,如果需要编译sass,es6,tpl,请使用[`jdf build`](a_tool_build.md)。 33 | * 建议使用`jdf server`来做简单的原型开发,demo测试,开发项目选用`jdf build`。 34 | * 利用`jdf server -w`开发过程中在浏览器实时预览静态文件的改动,解放F5。 35 | 36 | ## THANKS 37 | * 感谢[browserSync](https://github.com/browsersync/browser-sync)提供底层服务支持 38 | 39 | -------------------------------------------------------------------------------- /doc/core_css_optimize.md: -------------------------------------------------------------------------------- 1 | # css优化策略 2 | 3 | ## combo策略 4 | 5 | ``` 6 | {%widget name="a"%} 7 | {%widget name="b"%} 8 | ``` 9 | 10 | 编译后 11 | 12 | ``` 13 | 14 | ``` 15 | 16 | 相关配置参数 17 | 18 | ``` 19 | "output":{ 20 | "comboItemCount":2, //在同一个文件夹中,如果js或css文件数多余次数字,则会 combo 21 | "cssCombo": true, //css进行combo 22 | "jsCombo": true, //js进行combo 23 | } 24 | ``` 25 | 26 | ## 压缩css代码 27 | 28 | ``` 29 | .test1{ 30 | background: url(i/test1.png); 31 | } 32 | ``` 33 | 34 | 压缩后 35 | 36 | ``` 37 | .test1{background:url(i/test1.png)} 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /doc/core_plugin.md: -------------------------------------------------------------------------------- 1 | # 插件系统 2 | 3 | ## 目的 4 | 为jdf编译流程加入自定义的一些功能 5 | 6 | ## 可选插件 7 | * [jdf-cms](https://www.npmjs.com/package/jdf-cms) 8 | * [jdf-extract-template](https://www.npmjs.com/package/jdf-extract-template) 9 | 10 | ## 使用方法 11 | ### 插件集成 12 | #### 本地安装 13 | 在jdf工程根目录下执行`npm install pluginName --save-dev`,方便快捷,但是每新建一个工程就需要重新install。 14 | 15 | #### 全局安装 16 | 执行`npm install -g pluginName`全局安装后,环境变量中添加NODE_PATH,这样jdf就可以读取全局的plugin了 17 | 18 | [如何添加NODE_PATH](https://stackoverflow.com/questions/15636367/nodejs-require-a-global-module-package) 19 | 20 | ### 添加到jdf配置 21 | 详见“config.json配置项说明” 22 | 23 | ## 暴露的编译节点 24 | * 编译开始前`beforeBuild` 25 | * 编译完成后`afterBuild` 26 | * widget模板编译前`beforeTplRender` 27 | * widget模板插入html页面前`beforeTplInsert` 28 | 29 | 后续可以提供更多的编译节点。 30 | 31 | ## `config.json`配置项说明 32 | 在json文件顶层新增plugins属性,plugins是一个数组列表,每一个数组元素为一个插件配置,插件配置可以直接写插件名,也可以以对象的形式传递,目前受到规范的只有`name`属性,代表了插件名。 33 | 34 | 插件在各个节点的执行顺序为plugins数组声明顺序 35 | 36 | ``` JSON 37 | { 38 | "plugins": [ 39 | { 40 | "name": "jdf-cms" 41 | }, 42 | "jdf-extract-template" 43 | ] 44 | } 45 | 46 | ``` 47 | 48 | ## 插件约束 49 | 编写插件要遵循一些规范,以便被jdf模块系统初始化。 50 | ### 暴露方法 51 | 插件必须要暴露一个名为`Plugin`的函数,这个函数返回一个对象,对象里必须包含`setConfig`方法,该方法会在插件`require`到jdf中时第一时间执行,因此该方法也是插件的初始化方法,初始化工作可以放在这里执行。`setConfig`的`option`参数在下面单独说明。 52 | 53 | 除了`setConfig`方法以外,jdf编译节点钩子方法的声明也处于其中,可以只声明需要的钩子方法。 54 | 55 | 示例: 56 | ``` 57 | export const Plugin = function () { 58 | return { 59 | setConfig: function (option) { 60 | Object.assign(config, option || {}) 61 | }, 62 | beforeBuild: function () { 63 | return Promise.resolve() 64 | }, 65 | afterBuild: function () { 66 | return Promise.resolve() 67 | }, 68 | beforeTplRender: function (tpl, widgetInfo) { 69 | return tpl 70 | }, 71 | beforeTplInsert: function (tpl, widgetInfo) { 72 | return tpl 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ## jdf提供属性 79 | jdf工具提供了几个内部变量给插件使用。 80 | 81 | 通过`setConfig`传递。当前提供的属性有两个`jdf`, `VFS`,前者包含jdf配置项,后者为jdf的文件系统。 82 | -------------------------------------------------------------------------------- /doc/core_smarty.md: -------------------------------------------------------------------------------- 1 | # smarty模版 2 | 3 | ## 设计原则 4 | 让前端来写后端的smarty模板,并且前端不需要搭建各种繁杂的后端环境,另外模板的数据源可以在项目开始前前后端约定之后生成JSON文件,从而使两个角色并行开发。 5 | 6 | ## 使用方法: 7 |
1、将widget中的模版扩展名改为`.smarty`
8 |
2、将模版需要渲染的数据,放在当前widget下的`.json`文件或者widget标签的data属性中
9 |
3、引用方式:`{%widget name="test"%}`
10 |
4、单独引用模版:`{%widget name="test" tydive="smarty"%}`
11 | 12 | ## smarty基本语法 13 | * 变量 14 | Smarty有几种不同类型的变量,变量的类型取决于它的前缀是什么符号(或者被什么符号包围) 15 | Smarty的变量可以直接被输出或者作为函数属性和修饰符(modifiers)的参数,或者用于内部的条件表达式等等。 16 | 如果要输出一个变量,只要用定界符将它括起来就可以,例如: 17 | ```text 18 | {$Name} 19 | {$Contacts[row].Phone} 20 | ``` 21 | 22 | * if,else判断 23 | ```text 24 | {if $name eq "Fred"} 25 | Welcome Sir. 26 | {elseif $name eq "Wilma"} 27 | Welcome Ma'am. 28 | {else} 29 | Welcome, whatever you are. 30 | {/if} 31 | ``` 32 | 33 | * selection循环遍历 34 | 模板的section用于遍历数组中的数据。section标签必须成对出现,必须设置name和 loop属性。名称可以是包含字母、数字和下划线的任意组合,可以嵌套但必须保证嵌套的 name唯一。 35 | 变量loop(通常是数组)决定循环执行的次数。当需要在section循环内输出变量时,必须在变量后加上中括号包含着的name变量。 36 | ```text 37 | {section name=customer loop=$custid} 38 | id: {$custid[customer]}
39 | {/section} 40 | 41 | OUTPUT: 42 | 43 | id: 1000
44 | id: 1001
45 | id: 1002
46 | ``` -------------------------------------------------------------------------------- /doc/core_tpl.md: -------------------------------------------------------------------------------- 1 | # tpl模版 2 | 3 | ## 变量 4 | 5 | var data = {name: 'lilei'}; 6 | var str = "

<%=name%>

"; 7 | console.log($.tpl(str, data)); // =>

lilei

8 | 9 | ## 循环 10 | 11 | var data2 = { 12 | title:'listArray', 13 | list:[ 14 | { username:'tom',sex:1}, 15 | { username:'lili',sex:0} 16 | ] 17 | }; 18 | 19 | var str2 = '
' 20 | +'

<%=title%>

' 21 | +' ' 29 | +'
'; 30 | 31 | console.log($.tpl(str2, data2)); 32 | -------------------------------------------------------------------------------- /doc/core_vm.md: -------------------------------------------------------------------------------- 1 | # vm模版 2 | 3 | ## 设计原则 4 | 让前端来写后端的vm模板,并且前端不需要搭建各种繁杂的后端环境,前后端以 .vm 为沟通桥梁,另外模板的数据源可以在项目开始前前后端约定之后生成JSON文件,从而使两个角色并行开发。 5 | 6 | ## velocity模板引擎 7 | velocity模板语法的javascript实现,Velocity是基于Java的模板引擎,应用广泛。Velocity模板适用于大量模板使用的场景,支持模板嵌套,复杂的逻辑运算,包含基本数据类型、变量赋值和函数等功能。 8 | 9 | ## 目录结构 10 | * html/vm.html 11 | * widget/vm/vm.json 12 | * widget/vm/vm.vm 13 | 14 | ## 引用方法 15 | 16 | {%widget name="vm" data='{"name":"myname"}'%} 17 | 18 | 注意data之间的单双引号,data内容必须为json类型 19 | 20 | ## 数据源分类 21 | * data传参数据源,如 {%widget name="vm" data='{"name":"myname"}' %} 中的{"name":"myname"} 22 | * 数据源文件,如widget/vm/vm.json的内容 23 | * 两者优先级 "data传参数据源" > "数据源文件",即data传参数据源和数据源文件,数据名称相同时,以"data传参数据源"为准 24 | 25 | ## velocity基本语法 26 | 27 | * 1."#"用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#iinclude、#parse、#macro等,如: 28 | 29 | #if($info.images) 30 | 31 | #else 32 | 33 | #end 34 | 35 | * 2."$"用来标识一个变量,如 36 | 37 | $i、$msg.errorNum 38 | 39 | * 3."!"用来强制把不存在的变量显示为空白 40 | 41 | $!msg 42 | 43 | * 4.注释,如: 44 | 45 | ## 这是一行注释,不会输出 46 | 47 | ## velocity语法详解 48 | 49 | 具体更详细的语法可参考[官网] (http://velocity.apache.org/engine/devel/user-guide.html) 50 | 如vm.vm 51 | 52 | 53 | * 1.变量赋值输出 54 | 55 | Welcome $name to Javayou.com! 56 | today is $date. 57 | tdday is $mydae.//未被定义的变量将当成字符串 58 | 59 | * 2.设置变量值,所有变量都以$开头 60 | 61 | #set( $iAmVariable = "good!" ) 62 | Welcome $name to Javayou.com! 63 | today is $date. 64 | $iAmVariable 65 | 66 | * 3.if,else判断 67 | 68 | #set ($admin = "admin") 69 | #set ($user = "user") 70 | #if ($admin == $user) 71 | Welcome admin! 72 | #else 73 | Welcome user! 74 | #end 75 | 76 | * 4.迭代数据List ($velocityCount为列举序号,默认从1开始) 77 | 78 | #foreach( $product in $allProducts ) 79 |
  • $velocityCount $product.title
  • 80 | #end 81 | 82 | * 5.迭代数据get key 83 | 84 | #foreach($key in $myProducts.keySet() ) 85 | $key `s value: $myProducts.get($key) 86 | #end 87 | 88 | * 6.导入其它文件,可输入多个 89 | 90 | #parse("vm_a.vm") 91 | #parse("vm_b.vm") 92 | 93 | * 7.[todo多个文件用逗号隔开] 94 | 95 | * 8.简单遍历多个div 96 | #foreach( $i in [1,2,3,4] ) 97 |
    $i
    98 | #end 99 | 100 | 101 | 102 | ## 数据源举例 103 | 104 | 如vm.json 105 | 106 | { 107 | "name":"vm name", 108 | "allProducts":[ 109 | { 110 | "title": "风", 111 | "from": "中国" 112 | }, 113 | { 114 | "title": "应用", 115 | "from": "河北" 116 | } 117 | ], 118 | "myProducts":{ 119 | "age":9, 120 | "from":"cn" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /doc/core_widget.md: -------------------------------------------------------------------------------- 1 | # widget说明 2 | 3 | ## 什么是widget 4 | 5 | 在一个比较大型的项目中,一定会拥有很多页面,页面之间会有很多相似的模块,我们就可以把这些功能相似或者样式相似的模块抽离出来,形成一个widget。在需要的html页面中引入即可,引用语法如下: 6 | 7 | ``` 8 | {%widget name="test"%} 9 | ``` 10 | 11 | 在jdf工程中,创建一个widget使用`jdf widget --create xxx`命令,它会默认存在于当前项目的`widget`目录中,创建过程如下所示: 12 | 13 | ``` 14 | if you want to create it, type 'y', else 'n' 15 | vm: y 16 | js: y 17 | scss: y 18 | json: y 19 | ``` 20 | 21 | 一个标准的`widget`模块会默认包含上面四种文件,并且文件名默认和当前`widget`同名。如果只想引用vm文件,使用语法: 22 | 23 | ``` 24 | {%widget name="test" type="vm"%} 25 | ``` 26 | 27 | 只想引用css文件,使用语法: 28 | 29 | ``` 30 | {%widget name="test" type="css"%} 31 | ``` 32 | 33 | 同时只引用`vm`和`css`文件,使用语法: 34 | 35 | ``` 36 | {%widget name="test" type="vm,css"%} 37 | ``` 38 | 39 | ## vm文件 40 | 41 | `vm`是widget的模版文件,可以把其扩展名自行改成`tpl`,默认都支持`velocity`,`smarty`等模版引擎,当然,你也可以直接在其中编写html代码。 42 | 43 | ## css文件 44 | 45 | jdf默认创建的是scss文件,你可以使用less语法来编写css代码。 46 | 47 | ## js文件 48 | 49 | 在js文件中编写与此widget相关的js代码逻辑,支持使用ES6,jdf会自动编译成ES5。 50 | 51 | ## json文件 52 | 53 | widget的数据文件,直接书写`json`数据结构,在`vm`模版中使用类似`${name}`或者`<%=name%>`语法即可引用,具体需要看你使用的是哪种模版语法。 54 | 55 | ## 相关命令 56 | 57 | * `widget --preview [xxx]` - 预览所有项目中所有widget或部分widget 58 | * `widget --install xxx` - 安装一个widget模块到当前工程 59 | * `widget --publish xxx` - 发布一个widget模块到服务端 60 | * `widget --create xxx` - 在本地项目新建一个widget,会生成widget文件夹和vm,css,js,json文件 61 | * `widget --list` - 取得服务端所有widget列表 62 | 63 | -------------------------------------------------------------------------------- /doc/core_widgetoutputname.md: -------------------------------------------------------------------------------- 1 | # widgetOutputName标签 2 | 3 | ## 介绍 4 | 5 | widgetOutputName可以将页面中引用的`widget的js/css`内容合并,并在js文件夹或css文件夹中生成指定名称(myOutputName)的js/css文件,这样页面针对widget就只会引入一个资源文件。在服务器没有部署combo服务,或者前端需要频繁改动css文件时特别有用。 6 | 7 | ## 示例 8 | 9 | ``` 10 | page1.html page1.html(jdf output --debug) 11 | +-------------------------------------+ +----------------------------------+ 12 | | head | | head | 13 | | link.href=jdf/unit/2.0.0/base.css | | link.href=jdf/unit/2.0.0/base.css| 14 | | /head | | link.href=../css/myPureSource.css| 15 | | body | | /head | 16 | | {%widget="pureWidget1"%} | | body | 17 | | {%widget="pureWidget2"%} +->| | 18 | | {%widget="pureWidget3"%} | | script.src=../js/myPureSource.js | 19 | | {%widgetOutputName="myPureSource" %}| | /body | 20 | | /body | | | 21 | | | | | 22 | | | | | 23 | +-------------------------------------+ +----------------------------------+ 24 | ``` 25 | 26 | 如果给widgetOutputName指定type属性(css或js),那么只会将对应的type合并成myPureSource文件并引入。比如: 27 | 28 | ``` 29 | {%widgetOutputName="myPureSource" type="css" %} 30 | ``` 31 | 32 | 那么生成的页面就只会引用myPureSource.css,而不会引用myPureSource.js,js依旧引用widget中的js。 33 | 34 | ## 如何在多个页面引用同一个widgetOutputName? 35 | 36 | jdf会将各个页面设置的widgetOutputName都生成至./css目录中,因此,如果两个页面的widget不一致,而widgetOutputName同名的话,两个页面就会生成两个不同内容的css文件,就会导致后面生成的文件覆盖前面生成的文件。 37 | 38 | 从正常开发角度来说,如果两个页面引用了同名css,那么两个页面访问的css内容是一致的。如果想为每个页面单独生成一份独立的文件,请保证页面间widgetOutputName名不一致。如果想为页面引用同一个widgetOutputName,jdf也提供了一个配置选项。 39 | 40 | jdf提供在`config.json`文件中配置一个全局widgetOutputName的能力。所有页面均可以引用这个全局widgetOutputName。 41 | 42 | config.json 43 | 44 | ``` 45 | { 46 | "projectPath": "myProject", 47 | "widgetOutputName": "globalPureSource", 48 | "widgetOutputMode": 2, // 1: all widgets|2: white list|3: black list 49 | "widgetWhiteList": ["pureWidget1", "pureWidget3"], 50 | "widgetBlackList": ["pureWidget1", "pureWidget3"], 51 | } 52 | ``` 53 | 54 | myPage.html 55 | 56 | ``` 57 | {%widgetOutputName="globalPureSource" type="js,css" %} 58 | ``` 59 | 60 | 我们在config.json中定义了widgetOutputName字段,同时提供三个控制字段:widgetOutputMode, widgetWhiteList, widgetBlackList来控制全局widgetOutputName的文件生成。 61 | 62 | widgetOutputMode这个属性值可选`1|2|3`,1代表合并所有widget,2代表合并widgetWhiteList,3代表合并排除widgetBlackList的其他widget。 63 | -------------------------------------------------------------------------------- /lib/VFS/VirtualFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const logger = require('jdf-log'); 5 | const fileType = require('./fileType'); 6 | 7 | /** 8 | * 文件描述格式 9 | * originPath和targetPath均为绝对路径(absolute path) 10 | * originPath是从硬盘中文件的路径 11 | * targetPath是编译后生成的路径,位置和originPath处于同一文件夹下,后缀按需更改 12 | * originContent是文件内容,作为留存不直接在这上面操作 13 | * targetContent是处理后的文件内容,操作都是在targetContent上 14 | * fetched 文件内容是否已从硬盘拉取,拉取过为true 15 | * 示例: 16 | * VFile { 17 | * originPath: 'D:\\NodeApp\\jdfDev\\widget\\p2\\p2.scss', 18 | * originContent: '#p2 {\r\n a {\r\n color: blue;\r\n }\r\n}', 19 | * targetPath: 'D:\\NodeApp\\jdfDev\\widget\\p2\\p2.css', 20 | * targetContent: ''#p2 a {\n color: blue;\n}\n', 21 | * fetched: true 22 | * } 23 | */ 24 | 25 | class VFile { 26 | constructor(oPath, oContent, tPath, tContent) { 27 | if (oPath && !path.isAbsolute(oPath)) { 28 | logger.error('new VFile()--originPath must be absolute.'); 29 | return {}; 30 | } 31 | this._originPath = oPath; 32 | this._originContent = oContent; 33 | this._targetPath = tPath; 34 | this._targetContent = tContent; 35 | this._fetched = false; 36 | this._status = VFile.status.READY; 37 | } 38 | 39 | get status() { 40 | return this._status; 41 | } 42 | set status(value) { 43 | if (VFile.status[value]) { 44 | this._status = value; 45 | } else { 46 | logger.error('set status error, need status in VFile.status') 47 | } 48 | } 49 | 50 | get originPath() { 51 | return this._originPath; 52 | } 53 | 54 | set originPath(value) { 55 | this._originPath = value; 56 | } 57 | 58 | get originContent() { 59 | this.fetch(); 60 | return this._originContent; 61 | } 62 | 63 | set originContent(value) { 64 | this._fetched = true; 65 | this._originContent = value; 66 | } 67 | 68 | get targetPath() { 69 | if (!this._targetPath) { 70 | this._targetPath = this._originPath; 71 | } 72 | return this._targetPath; 73 | } 74 | 75 | set targetPath(value) { 76 | this._targetPath = value; 77 | } 78 | 79 | get targetContent() { 80 | return this._targetContent; 81 | } 82 | 83 | set targetContent(value) { 84 | this._fetched = true; 85 | this._targetContent = value; 86 | } 87 | 88 | // 如果没有读到内存,则执行一次读取操作 89 | // 无论成功失败,都只会读取一次 90 | fetch() { 91 | if (!this._fetched && !this._originContent) { 92 | this._originContent = fs.readFileSync(this._originPath); 93 | if(VFile.isTextFile(this)) { 94 | this._originContent = this._originContent.toString(); 95 | } 96 | this._targetContent = this._originContent; 97 | this._fetched = true; 98 | } 99 | } 100 | 101 | getType() { 102 | return path.extname(this._originPath).slice(1); 103 | } 104 | 105 | getTargetType() { 106 | if (!this._targetPath) { 107 | this._targetPath = this._originPath; 108 | } 109 | return path.extname(this._targetPath).slice(1); 110 | } 111 | 112 | changeTargetType(nType) { 113 | if (!this._targetPath) { 114 | this._targetPath = this._originPath; 115 | } 116 | this._targetPath = VFile.changeType(nType, this._targetPath); 117 | } 118 | 119 | // 如果是文本文件,那么可以以string方式写入内存,并成功写出 120 | // png, pdf, doc这种文件如果存string在内存处理,拷贝的时候就出问题 121 | // 可以在writeFies的时候指定输出文件后缀,避免非文本文件错误 122 | static isTextFile(vfile) { 123 | let textExtname = fileType.getTextExtname().join(','); 124 | let extname = vfile.getType(); 125 | 126 | let reg = new RegExp(`^${extname}$|\\W${extname}$|^${extname}\\W|\\W${extname}\\W`, 'i'); 127 | return reg.test(textExtname); 128 | } 129 | 130 | isTextFile() { 131 | return VFile.isTextFile(this); 132 | } 133 | 134 | isOutputIgnored() { 135 | let exts = fileType.getOutputIgnore().join(','); 136 | let extname = this.getType(); 137 | 138 | let reg = new RegExp(`^${extname}$|\\W${extname}$|^${extname}\\W|\\W${extname}\\W`, 'i'); 139 | return reg.test(exts); 140 | } 141 | 142 | static changeType(type, filepath) { 143 | let extname = path.extname(filepath).slice(1); 144 | return filepath.replace(new RegExp(extname + '$', 'i'), type); 145 | } 146 | } 147 | 148 | VFile.status = { 149 | READY: 'READY', 150 | DOING: 'DOING', 151 | DONE: 'DONE', 152 | WRITTEN: 'WRITTEN' 153 | } 154 | 155 | module.exports = VFile; 156 | -------------------------------------------------------------------------------- /lib/VFS/fileType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | let fileType = {}; 5 | 6 | // 允许读入VFS的文件类型 7 | fileType.extname = { 8 | // 文本文件类型 9 | js: ['js', 'ts', 'babel', 'es6'], 10 | css: ['css', 'scss', 'less', 'sass'], 11 | html: ['html', 'htm', 'xhtml'], 12 | vm: ['vm', 'tpl', 'smarty', 'jade'], 13 | data: ['json', 'xml', 'map'], 14 | 15 | // 非文本文件类型 16 | font: ['ttf', 'eot', 'woff', 'cur'], 17 | img: ['jpg', 'png', 'gif', 'jpeg', 'ico', 'swf', 'webp', 'svg', 'mp3', 'mp4', 'flv'], 18 | doc: ['txt', 'md', 'doc', 'docx', 'pdf', 'ppt', 'pptx', 'xls', 'xlsx'], 19 | zip: ['zip', 'rar', 'tar', 'war'] 20 | }; 21 | 22 | // 读取直接忽略的文件 23 | fileType.ignore = { 24 | dir: ['test', '..', '.git', '.svn', 'node_modules', 'Thumbs', 'DS_Store', '.db', 'bower_components', '.vscode', '.idea'], 25 | file: ['.*', 'config.json', 'package.json'], 26 | glob: [], 27 | extname: ['dll', 'ini', 'sys', 'exe'] 28 | } 29 | 30 | // 读取的时候需要读取,但不需要输出的文件 31 | fileType.outputIgnore = { 32 | extname: fileType.extname.vm.concat([]) 33 | } 34 | 35 | // ------------------extname-------------- 36 | 37 | // 可以在内存中当文本文件操作的类型 38 | fileType._textExtname = []; 39 | fileType.getTextExtname = function () { 40 | if (this._textExtname.length === 0) { 41 | this._textExtname = this.extname.js 42 | .concat(this.extname.css) 43 | .concat(this.extname.html) 44 | .concat(this.extname.vm) 45 | .concat(this.extname.data); 46 | } 47 | return this._textExtname; 48 | } 49 | 50 | fileType._PCExtname = []; 51 | fileType.getPCExtname = function () { 52 | if (this._PCExtname.length === 0) { 53 | this._PCExtname = this.getTextExtname() 54 | .concat(this.extname.img) 55 | .concat(this.extname.font); 56 | } 57 | return this._PCExtname; 58 | } 59 | 60 | fileType.isPCExtname = function (filename) { 61 | let extnames = fileType.getPCExtname().join(','); 62 | let extname = path.extname(filename).slice(1); 63 | let reg = new RegExp(`^${extname}$|\\W${extname}$|^${extname}\\W|\\W${extname}\\W`, 'i'); 64 | // 不符合的后缀过滤掉 65 | if (!reg.test(extnames)) { 66 | return false; 67 | } 68 | return true; 69 | }; 70 | 71 | // 获取所有能被VFS读取的文件类型 72 | fileType._allExtname = []; 73 | fileType.getAllExtname = function () { 74 | if (this._allExtname.length === 0) { 75 | let textArr = []; 76 | for (let ext in this.extname) { 77 | textArr = textArr.concat(this.extname[ext]); 78 | } 79 | this._allExtname = textArr; 80 | } 81 | return this._allExtname; 82 | } 83 | 84 | fileType.getRelativeType = function (type) { 85 | if (!this.extname[type]) { 86 | return []; 87 | } 88 | return this.extname[type]; 89 | } 90 | 91 | // ------------------ignore-------------- 92 | 93 | fileType.getIgnore = function () { 94 | let patterns = this.ignore.glob 95 | .concat(this.ignore.file) 96 | .concat(this.ignore.dir.map(item => { 97 | return item + '/**'; 98 | })) 99 | .concat(this.ignore.extname.map(item => { 100 | return '**/*.' + item; 101 | })); 102 | return patterns; 103 | } 104 | 105 | fileType.addIgnore = function (dirname, type) { 106 | if (type === 'glob') { 107 | this.ignore.glob = this.ignore.glob.concat(dirname); 108 | } else if (type === 'dir') { 109 | this.ignore.dir = this.ignore.dir.concat(dirname); 110 | } else if (type === 'file') { 111 | this.ignore.file = this.ignore.file.concat(dirname); 112 | } 113 | } 114 | 115 | fileType.getOutputIgnore = function () { 116 | // 需要的时候再扩展 117 | return fileType.outputIgnore.extname; 118 | } 119 | 120 | module.exports = fileType; 121 | -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jdfUtils = require('jdf-utils'); 4 | const f = jdfUtils.file; 5 | const logger = require('jdf-log'); 6 | const jdf = require('./jdf'); 7 | const path = require('path'); 8 | const vfs = require('./VFS/VirtualFileSystem'); 9 | /** 10 | * 直接修改vfs中的背景图为base64格式 11 | * @param vNode vfs指向的一个文件节点 12 | */ 13 | function convert(vNode) { 14 | const regBackground = /background(?:-image)?:([\s\S]*?)(?:;|$)/gi; 15 | const regImgUrl = /url\s*\(\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|[^)}]+)\s*\)/i; 16 | const regIsBase64 = /[?&]__base64/i; 17 | 18 | const source = vNode.originPath; // 要修改文件的绝对路径 19 | let content = vNode.targetContent; // 文件的原始内容 20 | const background = content.match(regBackground); 21 | 22 | if (background) { 23 | logger.verbose(`base64: ${source}`); 24 | content = encodeURIComponent(content); 25 | background.forEach((item) => { 26 | const imgUrl = item.match(regImgUrl); 27 | 28 | if (imgUrl && imgUrl[0].match(regIsBase64)) { 29 | const bgImgPath = path.join(path.dirname(source), imgUrl[1].replace('?__base64', '')); 30 | 31 | if (f.exists(bgImgPath)) { 32 | const base64Encode = f.base64Encode(bgImgPath); 33 | content = content.replace(new RegExp(encodeURIComponent(imgUrl[1]), 'gi'), ('data:image/png;base64,' + base64Encode)); 34 | }else{ 35 | logger.error(bgImgPath + ' is not exist'); 36 | } 37 | } 38 | }); 39 | vNode.targetContent = decodeURIComponent(content); 40 | } 41 | } 42 | 43 | module.exports.init = function() { 44 | logger.profile('base64'); 45 | vfs.queryFileByTargetType('css').forEach((item) => convert(item)); 46 | logger.profile('base64'); 47 | } 48 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const jdfUtils = require('jdf-utils'); 7 | const $ = jdfUtils.base; 8 | const f = jdfUtils.file; 9 | const logger = require('jdf-log'); 10 | const shelljs = require('shelljs'); 11 | const _ = require('lodash'); 12 | 13 | const jdf = require('./jdf.js'); 14 | const cssSprite = require('./cssSprite'); 15 | const base64 = require('./base64'); 16 | const buildCss = require("./buildCss"); 17 | const buildHTML = require("./buildHTML"); 18 | const buildHTMLDeep = require('./buildHTMLDeep') 19 | const buildOutputWidget = require('./buildOutputWidget'); 20 | const buildES6 = require('./buildES6'); 21 | const VFS = require('./VFS/VirtualFileSystem'); 22 | const bs = require('./server/browserSyncServer'); 23 | const middleware = require('./server/middlewareVFS'); 24 | const pluginCore = require('./pluginCore'); 25 | 26 | //exports 27 | const build = module.exports = {}; 28 | 29 | build.serverDir = ''; 30 | 31 | build.init = function (options) { 32 | let buildType = options.buildType; 33 | let serverDir = options.serverDir; 34 | build.serverDir = serverDir; 35 | let projectDir = options.projectDir; 36 | 37 | var logText = 'build files success'; 38 | 39 | //注册plugin, todo异步 40 | pluginCore.addPluginFromConfiguration(); 41 | 42 | return VFS.go() 43 | .then(() => { 44 | logger.profile('plugin.beforeBuild'); 45 | return pluginCore.excuteBeforeBuild(); 46 | }) 47 | .then(() => { 48 | logger.profile('plugin.beforeBuild'); 49 | return buildCss.init(); 50 | }) 51 | .then(() => { 52 | if (jdf.config.widgetNesting) { 53 | return buildHTMLDeep.init(options); 54 | } else { 55 | return buildHTML.init(options); 56 | } 57 | }) 58 | .then(() => { 59 | return buildES6.init(); 60 | }) 61 | .then(() => { 62 | base64.init(); 63 | }) 64 | .then(() => { 65 | cssSprite.init(); 66 | }) 67 | .then(() => { 68 | return buildOutputWidget.init(); 69 | }) 70 | .then(() => { 71 | logger.profile('plugin.afterBuild'); 72 | return pluginCore.excuteAfterBuild(); 73 | }) 74 | .then(() => { 75 | logger.profile('plugin.afterBuild'); 76 | 77 | logger.profile(options.profileText); 78 | logger.info(logText); 79 | }) 80 | .then(() => { 81 | let bsOptions = { 82 | autoOpen: false, 83 | watchDir: projectDir, 84 | serverDir: serverDir 85 | }; 86 | if (buildType === 'open') { 87 | bsOptions.autoOpen = true; 88 | 89 | } 90 | 91 | if (true) { 92 | return build.startVFSServer(bsOptions); 93 | } 94 | else { 95 | // 启动localserver,基于temp目录,性能低一些 96 | // 这个是用来做对比测试的,保证VFS server正确性,不提供watch功能 97 | logger.profile('delete temp files'); 98 | shelljs.rm("-Rf", jdf.transferDir); 99 | logger.profile('delete temp files'); 100 | return VFS.writeFilesToDir(jdf.transferDir).then(() => { 101 | return build.startLocalServer(bsOptions); 102 | }); 103 | } 104 | 105 | 106 | 107 | }) 108 | .catch(err => { 109 | logger.error(err); 110 | }); 111 | } 112 | 113 | build.rebuild = function (callback, option) { 114 | return VFS.go() 115 | .then(() => { 116 | logger.profile('plugin.beforeBuild'); 117 | return pluginCore.excuteBeforeBuild(); 118 | }) 119 | .then(() => { 120 | logger.profile('plugin.beforeBuild'); 121 | 122 | logger.profile('rebuild'); 123 | if (option && option.buildcss === false) { 124 | // 主动不编译css,等于false是为了避免 ===undefined 这种非主动情况 125 | return; 126 | } 127 | return buildCss.init(); 128 | }) 129 | .then(() => { 130 | if (option && option.buildwidget === false) { 131 | return; 132 | } 133 | if (jdf.config.widgetNesting) { 134 | return buildHTMLDeep.init(); 135 | } else { 136 | return buildHTML.init(); 137 | } 138 | }) 139 | .then(() => { 140 | if (option && option.buildjs === false) { 141 | return; 142 | } 143 | return buildES6.init(); 144 | }) 145 | .then(() => { 146 | if (option && option.buildcss === false) { 147 | return; 148 | } 149 | base64.init(); 150 | }) 151 | .then(() => { 152 | if (option && option.buildcss === false) { 153 | return; 154 | } 155 | cssSprite.init(); 156 | }) 157 | .then(() => { 158 | return buildOutputWidget.init(); 159 | }) 160 | .then(() => { 161 | logger.profile('plugin.afterBuild'); 162 | return pluginCore.excuteAfterBuild(); 163 | }) 164 | .then(() => { 165 | logger.profile('plugin.afterBuild'); 166 | 167 | logger.profile('rebuild'); 168 | // TODO 把arguments后面的参数传给callback 169 | callback && callback(); 170 | }) 171 | .catch(err => { 172 | logger.error(err); 173 | }); 174 | } 175 | 176 | build.debounceRebuild = function () {} 177 | 178 | build.startVFSServer = function (options) { 179 | return new Promise((resolve, project) => { 180 | // startup第一个参数是服务器根目录,基于VFS,不存在在内存中的文件从watchDir,也就是projectDir 181 | // 要改成middlewareLocal服务,则这个目录应该设置成jdf.transferDir 182 | // jdf server命令调用无参bs.startup(),用的middlewareLocal中间件服务,请参见。 183 | bs.startup(options.watchDir, { 184 | autoOpen: options.autoOpen, 185 | watchDir: options.watchDir, 186 | port: parseInt(jdf.config.localServerPort) 187 | }, function (port) { 188 | // save all的时候每save单个file都会触发一次reload,因此设置debounce,每次触发时只修改VFS 189 | // 设定保存一个文件的时间为300ms 190 | build.debounceRebuild = _.debounce(build.rebuild, 300); 191 | 192 | bs.watch(function (event, filename, reloadIt) { 193 | build.buildChangeFile(event, filename, reloadIt); 194 | }); 195 | resolve(port); 196 | }, middleware); 197 | }); 198 | } 199 | 200 | build.startLocalServer = function (options) { 201 | return new Promise((resolve, project) => { 202 | bs.startup(options.serverDir, {autoOpen: options.autoOpen, watchDir: options.watchDir}); 203 | }); 204 | } 205 | 206 | build.buildChangeFile = function (event, filename, reloadIt) { 207 | if (!jdf.config.build.livereload) { 208 | reloadIt = undefined; 209 | } 210 | 211 | if (event === 'update') { 212 | let newvfile = VFS.createFile(filename); 213 | VFS.updateFile(filename, newvfile.originContent); 214 | } else if (event === 'remove') { 215 | VFS.deleteFile(filename); 216 | VFS.deleteDir(filename); 217 | } 218 | 219 | this.debounceRebuild(reloadIt, this.buildOption(filename)); 220 | } 221 | 222 | /** 223 | * 引入按文件编译,在同时保存多个文件时存在风险,比如,最后保存的文件为js,那么就只会编译js 224 | * @param {*} filename 225 | */ 226 | build.buildOption = function (filename) { 227 | let option = { 228 | buildcss: true, 229 | buildjs: true, 230 | buildwidget: true 231 | } 232 | if (pluginCore.plugins.length > 0) { 233 | // 带插件全部编译 234 | return option; 235 | } 236 | 237 | let extname = path.extname(filename).replace('.', ''); 238 | 239 | let cssRelativeTypeArr = VFS.fileType.getRelativeType('css'); 240 | let jsRelativeTypeArr = VFS.fileType.getRelativeType('js'); 241 | let imgRelativeTypeArr = VFS.fileType.getRelativeType('img'); 242 | if (cssRelativeTypeArr.indexOf(extname) !== -1) { 243 | option.buildcss = true; 244 | option.buildjs = false; 245 | option.buildwidget = false; 246 | } 247 | else if (jsRelativeTypeArr.indexOf(extname) !== -1) { 248 | option.buildcss = false; 249 | option.buildjs = true; 250 | option.buildwidget = false; 251 | } 252 | else if (imgRelativeTypeArr.indexOf(extname) !== -1) { 253 | option.buildcss = false; 254 | option.buildjs = false; 255 | option.buildwidget = false; 256 | } 257 | else { 258 | option.buildcss = false; 259 | option.buildjs = false; 260 | option.buildwidget = true; 261 | } 262 | return option; 263 | } 264 | -------------------------------------------------------------------------------- /lib/buildCss.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const jdfUtils = require('jdf-utils'); 4 | const $ = jdfUtils.base; 5 | const f = jdfUtils.file; 6 | const logger = require('jdf-log'); 7 | 8 | //外部组件 9 | const Sass = require('node-sass'); 10 | const Less = require('less'); 11 | const postcss = require('postcss'); 12 | const autoprefixer = require('autoprefixer'); 13 | 14 | const jdf = require('./jdf'); 15 | const VFS = require('./VFS/VirtualFileSystem'); 16 | 17 | let buildCss = module.exports = {}; 18 | 19 | buildCss.init = function () { 20 | logger.profile('parse css'); 21 | return VFS.go() 22 | .then(() => { 23 | return VFS.travel((vfile, done) => { 24 | let oPath = vfile.originPath; 25 | if ($.is.sass(oPath)) { 26 | done.push(buildCss.handleSass(vfile)); 27 | } else if ($.is.less(oPath)) { 28 | done.push(buildCss.handleLess(vfile)); 29 | } else if ($.is.css(oPath)) { 30 | done.push(buildCss.handleCss(vfile)); 31 | } 32 | }); 33 | }) 34 | .then(() => { 35 | return VFS.travel((vfile, done) => { 36 | if (buildCss.isCssRelative(vfile)) { 37 | done.push(buildCss.postCSSProcess(vfile)); 38 | } 39 | }); 40 | }) 41 | .then(() => { 42 | logger.profile('parse css'); 43 | return Promise.resolve(); 44 | }); 45 | } 46 | 47 | buildCss.handleSass = function (vfile) { 48 | return new Promise (function (resolve, reject) { 49 | let oPath = vfile.originPath; 50 | logger.verbose(`parse sass: ${oPath}`); 51 | Sass.render({ 52 | file: oPath, 53 | outputStyle: 'expanded', 54 | importer: function(url, prev, done){ 55 | var extname = path.extname(url); 56 | 57 | if(extname == '.sass' || extname == '.scss' || extname == '.css'){ 58 | return {file: url}; 59 | }else{ 60 | return {file: url += (path.extname(prev))}; 61 | } 62 | 63 | } 64 | }, function (err, result) { 65 | if (err) { 66 | logger.error(err.formatted); 67 | reject(err); 68 | return; 69 | } 70 | vfile.targetContent = result.css.toString(); 71 | if (!vfile.targetPath) { 72 | vfile.targetPath = vfile.originPath; 73 | } 74 | vfile.targetPath = vfile.targetPath.replace(/scss$/, 'css'); 75 | resolve(); 76 | }); 77 | }); 78 | } 79 | 80 | buildCss.handleLess = function (vfile) { 81 | return new Promise(function (resolve, reject) { 82 | let lessContent; 83 | if (!vfile.originContent) { 84 | lessContent = f.read(vfile.originPath); 85 | } else { 86 | lessContent = vfile.originContent; 87 | } 88 | 89 | logger.verbose(`parse less: ${vfile.originPath}`); 90 | Less.render(lessContent, {filename: vfile.originPath, syncImport: true}) 91 | .then(function (output) { 92 | vfile.originContent = lessContent; 93 | vfile.targetContent = output.css; 94 | if (!vfile.targetPath) { 95 | vfile.targetPath = vfile.originPath; 96 | } 97 | vfile.targetPath = vfile.targetPath.replace(/less$/, 'css'); 98 | resolve(); 99 | }, function (err) { 100 | logger.error(`parse less file ${fileFullPath}`); 101 | console.log(err); 102 | reject(err); 103 | }); 104 | }); 105 | } 106 | 107 | buildCss.handleCss = function (vfile) { 108 | return vfile; 109 | } 110 | 111 | buildCss.postCSSProcess = function (vfile) { 112 | // 更多插件可以再扩展 113 | let cssAutoPrefixer = jdf.config.output.cssAutoPrefixer; 114 | let browsers = jdf.config.output.browserslist || []; 115 | let plugins = [autoprefixer({remove: cssAutoPrefixer, browsers: browsers})]; 116 | logger.verbose(`postcss - autoprefixer: parsed ${vfile.originPath}`); 117 | return new Promise(function (resolve, reject) { 118 | postcss(plugins) 119 | .process(vfile.targetContent) 120 | .then(result => { 121 | vfile.targetContent = result.css; 122 | resolve(); 123 | }) 124 | .catch(err => { 125 | reject(err); 126 | }); 127 | }); 128 | } 129 | 130 | buildCss.isCssRelative = function (vfile) { 131 | let oPath = vfile.originPath; 132 | if (!($.is.less(oPath) || $.is.sass(oPath) || $.is.css(oPath))) { 133 | return false; 134 | } 135 | return true; 136 | } 137 | 138 | -------------------------------------------------------------------------------- /lib/buildES6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 外部模块 4 | const path = require('path'); 5 | 6 | // jdf内置模块 7 | const jdfUtils = require('jdf-utils'); 8 | const $ = jdfUtils.base; 9 | const jdf = require('./jdf'); 10 | const vfs = require('./VFS/VirtualFileSystem'); 11 | const logger = require('jdf-log'); 12 | 13 | module.exports.init = () => { 14 | const files = vfs.queryFileByType(['babel', 'es6']); 15 | if(files.length > 0) { 16 | logger.profile('parse es6'); 17 | const babel = require('babel-core'); 18 | files.forEach((item) => { 19 | const result = babel.transform(item.originContent, { 20 | presets: $.uniq(jdf.config.babel.defaultPresets.concat(jdf.config.babel.presets || [])), 21 | plugins: $.uniq(jdf.config.babel.defaultPlugins.concat(jdf.config.babel.plugins || [])), 22 | sourceRoot: path.resolve(__dirname, '../node_modules'), 23 | sourceMaps: true 24 | }); 25 | logger.verbose(`build ES6: ${item.originPath}`); 26 | var fileName = path.basename(item.originPath, '.babel'); 27 | fileName = path.basename(fileName, '.es6'); 28 | const sourcemapFileName = `${fileName}.js.map`; 29 | item.targetContent = `${result.code}\n//# sourceMappingURL=./${sourcemapFileName}`; 30 | result.map.file = `${fileName}}.js`; 31 | logger.verbose(`generate sourcemap ${sourcemapFileName}`); 32 | const sourcemapFilePath = path.join(path.dirname(item.originPath), sourcemapFileName); 33 | vfs.addFile(sourcemapFilePath, JSON.stringify(result.map)); 34 | }); 35 | logger.profile('parse es6'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/buildHTML.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const fs = require('fs'); 6 | const jsmart = require('jsmart'); 7 | const logger = require('jdf-log'); 8 | const escapeStringRegexp = require('escape-string-regexp'); 9 | const stripJsonComments = require('strip-json-comments'); 10 | 11 | //lib自身组件 12 | const jdfUtils = require('jdf-utils'); 13 | const $ = jdfUtils.base; 14 | const f = jdfUtils.file; 15 | const jdf = require('./jdf'); 16 | const Vm = require("./vm"); 17 | const VFS = require('./VFS/VirtualFileSystem'); 18 | const widgetParser = require('./buildWidget'); 19 | const pluginCore = require('./pluginCore') 20 | 21 | let buildHTML = module.exports = {}; 22 | 23 | /** 24 | * 编译HTML 25 | * 在编译HTML之前,最好先编译完CSS、JS、vm/tpl/smarty模板,确保得到最正确的结果 26 | * @param {VFS} VFS 内存虚拟文件系统 27 | * @return 28 | */ 29 | buildHTML.init = function () { 30 | buildHTML.VFS = VFS; 31 | logger.profile('parse widget'); 32 | return VFS.go() 33 | .then(() => { 34 | return VFS.travel((vfile, done) => { 35 | // 只操作html文件 36 | if (!$.is.html(vfile.originPath)) { 37 | return; 38 | } 39 | 40 | logger.verbose(`reset vfile.targetContent to be vfile.originContent: ${vfile.originPath}`); 41 | // 由于要重新编译html,在这里重置targetContent 42 | vfile.targetContent = vfile.originContent; 43 | 44 | logger.verbose(`parse widget tag in html, generate widgetInfo object collection.`) 45 | let widgets = widgetParser.parseWidget(vfile.originContent); 46 | widgets.forEach(widgetInfo => { 47 | widgetInfo.pagename = path.basename(vfile.originPath); 48 | }); 49 | 50 | logger.verbose(`foreach widgetInfo`); 51 | // 编译单个widget到html中。下面两个map是为了css,js引用不重复插入html中 52 | let jsMap = new Map(); 53 | let cssMap = new Map(); 54 | widgets.forEach(widgetInfo => { 55 | logger.verbose(`parse widget: ${widgetInfo.name}`); 56 | let widgetVfiles = VFS.queryDir(widgetInfo.dirname); 57 | widgetVfiles.forEach(widgetVfile => { 58 | 59 | // 不处理widget中与widget规定不符的文件 60 | // 匹配\\widget\\widgetName\\widgetName.ext 61 | let oPath = widgetVfile.originPath; 62 | let oBasename = path.basename(oPath); 63 | let dirname = path.dirname(oPath); 64 | let widgetNameReg = new RegExp(escapeStringRegexp(widgetInfo.name) + '\.\\w+$'); 65 | if (path.relative(dirname, widgetInfo.dirname) 66 | || !widgetNameReg.test(oBasename)) { 67 | // 1、相对路径不为空 68 | // 2、文件名不和widget名一致 69 | return; 70 | } 71 | 72 | let wPath = widgetVfile.targetPath; 73 | let existMap = {jsMap: jsMap, cssMap: cssMap}; 74 | // widgetInfo.buildTag反映type中指定的编译类型 75 | if (widgetInfo.buildTag.vm && $.is.vm(wPath)) { 76 | logger.verbose(`insert vm into HTML: ${widgetVfile.originPath}`); 77 | buildHTML.insertVM(widgetInfo, vfile, widgetVfile, existMap); 78 | } 79 | else if (widgetInfo.buildTag.tpl && $.is.tpl(wPath)) { 80 | // 旧代码vm和tpl等价 81 | logger.verbose(`insert vm into HTML: ${widgetVfile.originPath}`); 82 | buildHTML.insertVM(widgetInfo, vfile, widgetVfile, existMap); 83 | } 84 | else if (widgetInfo.buildTag.smarty && $.is.smarty(wPath)){ 85 | logger.verbose(`insert smarty into HTML: ${widgetVfile.originPath}`); 86 | buildHTML.insertSmarty(widgetInfo, vfile, widgetVfile); 87 | } 88 | else if (widgetInfo.buildTag.css && $.is.css(wPath)){ 89 | if (cssMap.has(widgetInfo.name)) { 90 | logger.verbose(`have inserted widget ${widgetInfo.name}'s css in html`); 91 | } else { 92 | logger.verbose(`insert tag into HTML: ${widgetVfile.originPath}`); 93 | buildHTML.insertCSS(vfile, widgetVfile); 94 | cssMap.set(widgetInfo.name, true); 95 | } 96 | } 97 | else if (widgetInfo.buildTag.js && $.is.js(wPath)){ 98 | if (jsMap.has(widgetInfo.name)) { 99 | logger.verbose(`have inserted widget ${widgetInfo.name}'s js '; 12 | } 13 | 14 | exports.injectPosition = 'bodyStart'; -------------------------------------------------------------------------------- /lib/server/injector/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // TODO 可以从config.json里决定注入到哪个位置 3 | const config = { 4 | inject: 'bodyStart' // bodyEnd, headStart, headEnd 5 | } 6 | const dumpSeajsCombo = require('./dumpSeajsCombo'); 7 | 8 | /** 9 | * server运行期间往html里注入脚本 10 | * @param {*} html 11 | */ 12 | exports.injectHTML = function (html) { 13 | // injectors标准接口:getInjection:function, injectPosition:string 14 | let injectors = [dumpSeajsCombo]; 15 | 16 | injectors.forEach(function (injector) { 17 | let scpt = injector.getInjection(), 18 | position = injector.injectPosition; 19 | 20 | switch (position) { 21 | case 'bodyStart': 22 | html = html.replace(/(\)/, "$1" + scpt); 23 | break; 24 | case 'bodyEnd': 25 | html = html.replace(/(\<\/body\>)/, scpt + "$1"); 26 | break; 27 | case 'headStart': 28 | html = html.replace(/(\)/, "$1" + scpt); 29 | break; 30 | case 'headEnd': 31 | html = html.replace(/(\<\/head\>)/, scpt + "$1"); 32 | break; 33 | } 34 | }); 35 | 36 | return html; 37 | } -------------------------------------------------------------------------------- /lib/server/middlewareLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * 基于硬盘文件的服务中间件 4 | * 和VFS系统完全不相关 5 | * 在任意目录运行jdf server就是调用的这个middleware 6 | */ 7 | 8 | const url = require('url'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const f = require('jdf-utils').file; 12 | const logger = require('jdf-log'); 13 | 14 | const mime = require('./mime'); 15 | const Compress = require('../urlReplace'); 16 | const dirHTML = require('./middlewareVFS/res.dirView/dir.html'); 17 | 18 | let server = module.exports = {}; 19 | 20 | let config = server.config = {}; 21 | 22 | // enum 23 | let pathType = { 24 | combo: 1, 25 | dir: 2, 26 | file: 3, 27 | empty: 4 28 | }; 29 | 30 | server.init = function (serverDir, port) { 31 | port = port || 8080; 32 | config.port = port; 33 | 34 | config.serverDir = serverDir; 35 | 36 | return function (request, response, next) { 37 | let jdfRes = {}; 38 | let requestUrl = decodeURI(request.url); 39 | 40 | let resource = server.getResourceInfo(requestUrl); 41 | switch (resource.type) { 42 | case pathType.empty: jdfRes = server.Res404(); break; 43 | case pathType.combo: jdfRes = server.ResCombo(resource.filelist); break; 44 | case pathType.dir: jdfRes = server.ResDir(resource.filelist[0], url.parse(requestUrl).pathname); break; 45 | case pathType.file: jdfRes = server.ResFile(resource.filelist[0]); break; 46 | default: jdfRes = server.Res404(); 47 | } 48 | 49 | response.writeHead(jdfRes.status, { 50 | 'Content-Type': jdfRes.contentType 51 | }); 52 | response.write(jdfRes.content); 53 | response.end(); 54 | next(); 55 | } 56 | } 57 | 58 | /** 59 | * 根据请求url获取本地文件,目录路径 60 | * @param {String} requestUrl 请求url 61 | * @return {Object} 包含路径信息以及有关这个路径信息的元信息 62 | */ 63 | server.getResourceInfo = function (requestUrl) { 64 | requestUrl = requestUrl || ''; 65 | // example /a/b/c/ 66 | let parsedUrl = url.parse(requestUrl); 67 | let pathname = parsedUrl.pathname; 68 | let isComboUrl = /^\?\?/.test(parsedUrl.search); 69 | let fileNameList = []; 70 | let resource = {}; 71 | 72 | if (isComboUrl) { 73 | // ['a.js', 'b.js'] 74 | fileNameList = parsedUrl.query.slice(1).split(','); 75 | } else { 76 | // ['/a/b/c/'] 77 | fileNameList = [pathname]; 78 | } 79 | 80 | fileNameList = fileNameList.map(function (item) { 81 | // ['C://Users/xxx/AppData/Local/Temp/a.js'] 82 | return path.normalize(config.serverDir + '/' + decodeURI(item)); 83 | }); 84 | 85 | // type: combo 86 | if (isComboUrl) { 87 | resource.type = pathType.combo; 88 | resource.filelist = fileNameList; 89 | return resource; 90 | } 91 | 92 | let filepath = fileNameList[0]; 93 | let stat; 94 | try { 95 | stat = fs.statSync(filepath); 96 | } catch (e) { 97 | resource.type = pathType.empty; 98 | resource.filelist = []; 99 | return resource; 100 | } 101 | // type: 目录 102 | if (stat.isDirectory()) { 103 | resource.type = pathType.dir; 104 | resource.filelist = fileNameList; 105 | return resource; 106 | } 107 | 108 | // type: 文件 109 | if (stat.isFile()) { 110 | resource.type = pathType.file; 111 | resource.filelist = fileNameList; 112 | return resource; 113 | } 114 | 115 | resource.type = pathType.empty; 116 | resource.filelist = []; 117 | return resource; 118 | } 119 | 120 | /** 121 | * 未找到资源返回404 122 | * @return {Object} 包含状态吗,返回内容类型,返回内容的对象 123 | */ 124 | function response404() { 125 | let notfound = '

    404 Not Found


    '+server.copyright(config.port)+'
    '; 126 | return { 127 | status: 404, 128 | contentType: mime['html'], 129 | content: notfound 130 | }; 131 | } 132 | 133 | /** 134 | * 404处理函数 135 | * @type {[void]} 136 | */ 137 | server.Res404 = response404; 138 | 139 | /** 140 | * 文件资源处理函数 141 | * @param {String} resPath 文件资源路径 142 | * @return {Object} 包含状态吗,返回内容类型,返回内容的对象 143 | */ 144 | server.ResFile = function (resPath) { 145 | let ext = path.extname(resPath); 146 | ext = ext.replace('.', ''); 147 | let contentType = mime[ext] || mime['txt']; 148 | let content = fs.readFileSync(resPath); 149 | return { 150 | status: 200, 151 | contentType: contentType, 152 | content: content 153 | }; 154 | } 155 | 156 | /** 157 | * 目录访问处理函数 158 | * @param {String} resPath 目录路径 159 | * @param {String} urlPath url相对根域名的路径 160 | */ 161 | server.ResDir = function (resPath, urlPath) { 162 | let html = server.getDirList(resPath, urlPath); 163 | return { 164 | status: 200, 165 | contentType: mime['html'], 166 | content: html 167 | }; 168 | } 169 | 170 | /** 171 | * combo组装处理函数 172 | * @param {Array} resList 需要combo的文件列表 173 | * 备注2016-11-23:有require时解析依赖没有测试,这个测试应该放在compress.js中,正常文件combo是可以成功的 174 | */ 175 | server.ResCombo = function (resList) { 176 | let ext, type, content, res; 177 | 178 | if (resList && resList.length > 0) { 179 | ext = path.extname(resList[0]).slice(1); 180 | } 181 | type = mime[ext] || mime['txt']; 182 | 183 | content = server.getComboFilesContent(resList); 184 | 185 | if (content === undefined) { 186 | res = response404(); 187 | } else { 188 | res = { 189 | status: 200, 190 | contentType: type, 191 | content: content 192 | } 193 | } 194 | return res; 195 | } 196 | 197 | server.getIp = function(){ 198 | let net = require('os').networkInterfaces(); 199 | for(let key in net){ 200 | if(net.hasOwnProperty(key)){ 201 | let items = net[key]; 202 | if(items && items.length){ 203 | for(let i = 0; i < items.length; i++){ 204 | let ip = String(items[i].address).trim(); 205 | if(ip && /^\d+(?:\.\d+){3}$/.test(ip) && ip !== '127.0.0.1'){ 206 | return ip; 207 | } 208 | } 209 | } 210 | } 211 | } 212 | return '127.0.0.1'; 213 | }; 214 | 215 | server.copyright = function (port){ 216 | let serverIp = server.getIp()+':'+port; 217 | let copyright = '

    jdf server '+ 218 | ' IP '+serverIp+' '+ 219 | //''+new Date()+' '+ 220 | '

    '; 221 | return copyright; 222 | } 223 | 224 | /** 225 | * 拼接展示目录的页面 226 | * @param {String} realPath 目录路径 227 | * @param {String} pathname 相对于根域名的路径 228 | * @return {HTML String} 拼接的html片段 229 | */ 230 | server.getDirList = function(realPath){ 231 | let data = { 232 | title: '', 233 | projectName: '', 234 | pathname: '', 235 | files: [], 236 | dirs: [] 237 | }; 238 | 239 | realPath = path.normalize(realPath); 240 | let serverDir = config.serverDir; 241 | let pathname = path.relative(serverDir, realPath); 242 | 243 | data.projectName = path.basename(serverDir); 244 | data.title = data.projectName + ' File List'; 245 | data.pathname = pathname; 246 | data.pathname = data.pathname.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); 247 | 248 | // 返回上一级目录 249 | data.dirs.push({pathname: path.dirname(data.pathname), name: '..'}); 250 | 251 | let projAbsDir = realPath; 252 | // 不过滤任何内容 253 | fs.readdirSync(projAbsDir).forEach(name => { 254 | let absPath = path.join(projAbsDir, name); 255 | let pathname = path.relative(serverDir, absPath); 256 | 257 | // D:\\NodeApp\\jdfDev\\widget\\ -> (localhost:8080)/jdfDev/widget,用于浏览器地址栏,手动统一转为正斜杠 258 | pathname = pathname.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); 259 | logger.verbose(`file or dir relative path: ${pathname}`); 260 | 261 | if (f.isDir(absPath)) { 262 | data.dirs.push({pathname: pathname, name: name}); 263 | } else if (f.isFile(absPath)) { 264 | data.files.push({pathname: pathname, name: name}); 265 | } 266 | }); 267 | 268 | let html = dirHTML.html(data); 269 | return html; 270 | } 271 | 272 | /** 273 | * 拼接combo文件 274 | * @param {Array} combolist 需要combo的路径列表 275 | * @return {String} combo后的文件内容 276 | */ 277 | server.getComboFilesContent = function (combolist) { 278 | let comboContent = ''; 279 | combolist.forEach(function(file){ 280 | let content = ''; 281 | if(f.exists(file)){ 282 | content = f.read(file); 283 | if (path.extname(file) === '.js') { 284 | // 解析js依赖,没有解析css依赖 285 | content = f.read(file); 286 | if(typeof(Compress.addJsDepends) === 'function'){ 287 | content = Compress.addJsDepends(file); 288 | } 289 | //如果代码的末尾没有分号,则自动添加一个。以避免代码合并出现异常。 290 | if(!/[;\r\n]$/.test(content)){ 291 | content += ';'; 292 | } 293 | } 294 | comboContent += content; 295 | }else{ 296 | comboContent = undefined; 297 | return false; 298 | } 299 | }); 300 | return comboContent;; 301 | } 302 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/envConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | webRoot: '' 5 | } 6 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let mw = require('./middlewareVFS'); 4 | 5 | module.exports.init = mw.init; 6 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/middlewareVFS.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * 基于VFS系统的服务中间件 4 | * request会首先经过。。。。;。。。 5 | * @type {[type]} 6 | */ 7 | const url = require('url'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const f = require('jdf-utils').file; 11 | const mime = require('../mime'); 12 | const view = require('./view'); 13 | const VFS = require('../../VFS/VirtualFileSystem'); 14 | const logger = require('jdf-log'); 15 | 16 | let server = module.exports = {}; 17 | 18 | // enum 19 | let pathType = { 20 | combo: 1, 21 | dir: 2, 22 | file: 3, 23 | empty: 4 24 | }; 25 | 26 | server.init = function (projectDir) { 27 | view.config.webRoot = path.normalize(projectDir); 28 | 29 | return function (request, response, next) { 30 | let jdfRes = {}; 31 | let requestUrl = decodeURI(request.url); 32 | 33 | let resource = server.getResourceInfo(requestUrl); 34 | logger.verbose(`resource info: ${JSON.stringify(resource)}`); 35 | 36 | switch (resource.type) { 37 | case pathType.empty: jdfRes = server.Res404(); break; 38 | case pathType.combo: jdfRes = server.ResCombo(resource.filelist); break; 39 | case pathType.dir: jdfRes = server.ResDir(resource.filelist[0]); break; 40 | case pathType.file: jdfRes = server.ResFile(resource.filelist[0]); break; 41 | default: jdfRes = server.Res404(); 42 | } 43 | 44 | response.writeHead(jdfRes.status, { 45 | 'Content-Type': jdfRes.contentType 46 | }); 47 | response.write(jdfRes.content); 48 | response.end(); 49 | next(); 50 | } 51 | } 52 | 53 | /** 54 | * 根据请求url获取本地文件,目录路径 55 | * @param {String} requestUrl 请求url 56 | * @return {Object} 包含路径信息以及有关这个路径信息的元信息 57 | */ 58 | server.getResourceInfo = function (requestUrl) { 59 | requestUrl = requestUrl || ''; 60 | // example /a/b/c/ 61 | let parsedUrl = url.parse(requestUrl); 62 | let pathname = parsedUrl.pathname; 63 | let isComboUrl = /^\?\?/.test(parsedUrl.search); 64 | let fileNameList = []; 65 | let resource = {}; 66 | 67 | if (isComboUrl) { 68 | // ['a.js', 'b.js'] 69 | fileNameList = parsedUrl.query.slice(1).split(','); 70 | 71 | fileNameList = fileNameList.map(function (item) { 72 | // ['C://Users/xxx/AppData/Local/Temp/a.js'] 73 | return path.join(view.config.webRoot, pathname, decodeURI(item)); 74 | }); 75 | } else { 76 | // ['/a/b/c/'] 77 | fileNameList = [pathname]; 78 | fileNameList = fileNameList.map(function (item) { 79 | // ['C://Users/xxx/AppData/Local/Temp/a.js'] 80 | return path.join(view.config.webRoot, decodeURI(item)); 81 | }); 82 | } 83 | 84 | // type: combo 85 | if (isComboUrl) { 86 | 87 | resource.type = pathType.combo; 88 | } else if (f.isDir(fileNameList[0])) { 89 | // type: 目录 90 | resource.type = pathType.dir; 91 | } else { 92 | // defaults: file 93 | resource.type = pathType.file; 94 | } 95 | 96 | resource.filelist = fileNameList; 97 | return resource; 98 | } 99 | 100 | /** 101 | * 未找到资源返回404 102 | * @return {Object} 包含状态吗,返回内容类型,返回内容的对象 103 | */ 104 | function response404() { 105 | let notfound = view.res404(); 106 | return { 107 | status: 404, 108 | contentType: mime['html'], 109 | content: notfound 110 | }; 111 | } 112 | 113 | /** 114 | * 404处理函数 115 | * @type {[void]} 116 | */ 117 | server.Res404 = response404; 118 | 119 | /** 120 | * 文件资源处理函数 121 | * @param {String} resPath 文件资源路径 122 | * @return {Object} 包含状态吗,返回内容类型,返回内容的对象 123 | */ 124 | server.ResFile = function (resPath) { 125 | let ext = path.extname(resPath); 126 | ext = ext.replace('.', ''); 127 | let contentType = mime[ext] || mime['txt']; 128 | 129 | let content = view.fileRes.getFileContent(resPath); 130 | 131 | // 定义null为not found 132 | if (content === null) { 133 | return this.Res404(); 134 | } 135 | 136 | return { 137 | status: 200, 138 | contentType: contentType, 139 | content: content 140 | }; 141 | } 142 | 143 | /** 144 | * 目录访问处理函数 145 | * @param {String} resPath 目录路径 146 | * @param {String} urlPath url相对根域名的路径 147 | */ 148 | server.ResDir = function (resPath) { 149 | let html = view.dirView.genHTML(resPath); 150 | return { 151 | status: 200, 152 | contentType: mime['html'], 153 | content: html 154 | }; 155 | } 156 | 157 | /** 158 | * combo组装处理函数 159 | * @param {Array} resList 需要combo的文件列表 160 | * 备注2016-11-23:有require时解析依赖没有测试,这个测试应该放在compress.js中,正常文件combo是可以成功的 161 | */ 162 | server.ResCombo = function (resList) { 163 | let ext, type, content, res; 164 | 165 | if (resList && resList.length > 0) { 166 | ext = path.extname(resList[0]).slice(1); 167 | } 168 | type = mime[ext] || mime['txt']; 169 | 170 | content = view.comboRes.getComboContent(resList); 171 | 172 | if (content === undefined) { 173 | res = response404(); 174 | } else { 175 | res = { 176 | status: 200, 177 | contentType: type, 178 | content: content 179 | } 180 | } 181 | return res; 182 | } 183 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.404.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const res404 = module.exports = {}; 4 | 5 | res404.res404 = function () { 6 | return ` 7 | 8 | 9 | 10 | 11 | 12 | 13 | 访问的资源不存在 14 | 15 | 16 | 17 |

    404 Not Found

    18 |
    19 | 返回主页 20 | 21 | `; 22 | } 23 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.comboContent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const f = require('jdf-utils').file; 6 | const VFS = require('../../VFS/VirtualFileSystem'); 7 | const resFile = require('./res.file'); 8 | 9 | const combo = module.exports = {}; 10 | 11 | /** 12 | * 拼接combo文件 13 | * @param {Array} combolist 需要combo的路径列表 14 | * @return {String} combo后的文件内容 15 | */ 16 | combo.getComboContent = function (combolist) { 17 | let comboContent = ''; 18 | let allExist = true; 19 | combolist.forEach(filepath => { 20 | filepath = path.normalize(filepath); 21 | let vfile = resFile.getTargetVFile(filepath); 22 | if (!allExist || !vfile) { 23 | allExist = false; 24 | return; 25 | } 26 | 27 | comboContent += vfile.targetContent; 28 | 29 | if (path.extname(filepath) === '.js') { 30 | // 添加分号 以避免代码合并出现异常。 31 | comboContent += ';'; 32 | } 33 | }); 34 | 35 | if (allExist) { 36 | return comboContent; 37 | } else { 38 | return undefined; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/dir.css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let iconfont = ` 4 | @font-face {font-family: "iconfont"; 5 | src: url('https://raw.githubusercontent.com/jdf2e/jdf/master/lib/server/middlewareVFS/res.dirView/iconfont.eot?t=1481534148992'); /* IE9*/ 6 | src: url('https://raw.githubusercontent.com/jdf2e/jdf/master/lib/server/middlewareVFS/res.dirView/iconfont.eot?t=1481534148992#iefix') format('embedded-opentype'), /* IE6-IE8 */ 7 | url('https://raw.githubusercontent.com/jdf2e/jdf/master/lib/server/middlewareVFS/res.dirView/iconfont.woff?t=1481534148992') format('woff'), /* chrome, firefox */ 8 | url('https://raw.githubusercontent.com/jdf2e/jdf/master/lib/server/middlewareVFS/res.dirView/iconfont.ttf?t=1481534148992') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 9 | url('https://raw.githubusercontent.com/jdf2e/jdf/master/lib/server/middlewareVFS/res.dirView/iconfont.svg?t=1481534148992#iconfont') format('svg'); /* iOS 4.1- */ 10 | } 11 | .iconfont { 12 | font-family:"iconfont" !important; 13 | font-size:16px; 14 | font-style:normal; 15 | -webkit-font-smoothing: antialiased; 16 | -webkit-text-stroke-width: 0.2px; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | .icon-file:before { content: "\e66f"; } 20 | .icon-wenjianjia:before { content: "\e7a0"; }`; 21 | 22 | let base = ` 23 | html, body, div, span, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, address, big, cite, code, del, em, font, img, ins, small, strong, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | body { 29 | font-family: Consolas,Microsoft YaHei,Arial,sans-serif; 30 | font-size: 16px; 31 | line-height: 1.42857143; 32 | color: #333; 33 | background-color: #fff; 34 | padding-top: 72px; 35 | background: #fff; 36 | } 37 | 38 | ol, ul { 39 | list-style: none; 40 | } 41 | 42 | a { 43 | color: #666; 44 | text-decoration: none; 45 | }`; 46 | 47 | let header = ` 48 | .header-wrap { 49 | background: #eee; 50 | position: fixed; 51 | left: 0; 52 | top: 0; 53 | width: 100%; 54 | z-index: 999; 55 | } 56 | 57 | .page-header { 58 | margin: 0 auto; 59 | width: 100%; 60 | max-width: 900px; 61 | padding-left: 15px; 62 | padding-top: 8px; 63 | box-sizing: border-box; 64 | } 65 | 66 | .page-header .header-text { 67 | line-height: 40px; 68 | font-size: 18px; 69 | }`; 70 | 71 | let content = ` 72 | .page-content { 73 | margin: 0 auto; 74 | width: 100%; 75 | max-width: 900px; 76 | } 77 | 78 | .dir-list-header { 79 | padding: 10px 15px; 80 | font-weight: bold; 81 | font-size: 14px; 82 | } 83 | 84 | .dir-list { 85 | padding-bottom: 100px; 86 | } 87 | 88 | .dir-list li { 89 | display: block; 90 | box-sizing: border-box; 91 | } 92 | 93 | .dir-list li a { 94 | position: relative; 95 | display: block; 96 | padding: 10px 15px; 97 | border-bottom: 1px solid #eee; 98 | text-decoration: none; 99 | } 100 | 101 | .dir-list li a:hover { 102 | background-color: #eee; 103 | text-decoration: none; 104 | color: #333; 105 | } 106 | 107 | .dir-list li .icon { 108 | display: inline-block; 109 | margin-right: 5px; 110 | } 111 | 112 | .dir-list li .icon.i-dir { 113 | color: #ff6309; 114 | }`; 115 | 116 | let footer = ` 117 | .footer { 118 | position: fixed; 119 | bottom: 0px; 120 | width: 100%; 121 | text-align: center; 122 | background: #eee; 123 | }`; 124 | 125 | let qrcode = ` 126 | .qrcode-expand { 127 | position: fixed; 128 | right: 2%; 129 | top: 48px; 130 | width: 144px; 131 | background: #fff; 132 | z-index: 9; 133 | } 134 | 135 | .qrcode-expand .deco-left, .qrcode-expand .deco-right, .qrcode-expand .qrcode-btn { 136 | float: right; 137 | height: 24px; 138 | line-height: 24px; 139 | background: #7a6e6e; 140 | cursor: pointer; 141 | } 142 | 143 | .qrcode-expand .up-btn { 144 | width: 100%; 145 | height: 24px; 146 | padding: 0; 147 | line-height: 24px; 148 | border: 0 none; 149 | background: #7a6e6e; 150 | font-size: 14px; 151 | color: #ffe; 152 | border-radius: 4px; 153 | cursor: pointer; 154 | } 155 | 156 | .qrcode-expand .deco-right, .qrcode-expand .deco-left { 157 | width: 24px; 158 | background: #7a6e6e; 159 | } 160 | 161 | .qrcode-expand .deco-right div, .qrcode-expand .deco-left div { 162 | width: 24px; 163 | height: 24px; 164 | background: #fff; 165 | } 166 | 167 | .qrcode-expand .deco-right div { 168 | border-top-right-radius: 11px; 169 | } 170 | 171 | .qrcode-expand .deco-left div { 172 | border-top-left-radius: 11px; 173 | } 174 | 175 | .qrcode-expand .qrcode-btn { 176 | width: 80px; 177 | background: #7a6e6e; 178 | text-align: center; 179 | color: #ffe; 180 | border-radius: 0 0 15px 15px; 181 | -webkit-user-select: none; 182 | -moz-user-select: none; 183 | -ms-user-select: none; 184 | user-select: none; 185 | } 186 | 187 | .qrcode-expand .qrcode-wrap { 188 | display: none; 189 | float: left; 190 | width: 144px; 191 | padding-top: 5px; 192 | } 193 | 194 | .qrcode-expand .tips { 195 | font-size: 12px; 196 | color: red; 197 | }`; 198 | 199 | module.exports = ` 200 | ${iconfont} 201 | ${base} 202 | ${header} 203 | ${content} 204 | ${qrcode} 205 | ${footer} 206 | `; 207 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/dir.html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const css = require('./dir.css'); 3 | 4 | module.exports = {}; 5 | 6 | let dirHTML = module.exports = {}; 7 | 8 | dirHTML.DataExample = { 9 | title: 'directory', 10 | projectName: 'projectName', 11 | pathname: '', 12 | files: [{pathname: '', name: ''}], 13 | dirs: [{pathname: '', name: ''}] 14 | } 15 | 16 | dirHTML.html = function (data) { 17 | return ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | ${data.title} 25 | 26 | 27 | 28 | 31 | 32 | 33 |
    34 | 37 |
    38 |
    39 |
    40 |
    41 |
    ${data.pathname}/
    42 |
    43 |
    44 |
    45 |
      46 | ${dirHTML.genDirList(data.dirs)} 47 | ${dirHTML.genFileList(data.files)} 48 |
    49 |
    50 |
    51 | 54 | ${dirHTML.qrcode} 55 | 56 | `; 57 | } 58 | 59 | dirHTML.qrcode = `
    60 |
    二维码
    61 |
    62 | 手机访问本页面 63 |
    64 |
    65 |

    顺利访问tips:

    66 |

    # 当前url不是localhost或127.0.0.1

    67 |

    # 手机电脑在同一网络

    68 |

    # 选择Access URLs: External网址

    69 |

    # build -o,自动打开的网址就是External网址

    70 |

    # weinre调试:3001/remote-debug

    71 |
    72 | 73 |
    74 |
    75 | 76 | `; 96 | 97 | dirHTML.genDirList = function (list) { 98 | if (!list) { 99 | return ''; 100 | } 101 | 102 | list = list.sort((a, b) => { 103 | if (a.name > b.name) { 104 | return 1; 105 | } 106 | return -1; 107 | }); 108 | 109 | let str = ''; 110 | list.forEach(item => { 111 | str += `
  • ${item.name}/
  • `; 112 | }); 113 | return str; 114 | } 115 | 116 | dirHTML.genFileList = function (list) { 117 | if (!list) { 118 | return ''; 119 | } 120 | 121 | list = list.sort((a, b) => { 122 | if (a.name > b.name) { 123 | return 1; 124 | } 125 | return -1; 126 | }); 127 | 128 | let str = ''; 129 | list.forEach(item => { 130 | str += `
  • ${item.name}
  • `; 131 | }); 132 | return str; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/lib/server/middlewareVFS/res.dirView/iconfont.eot -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Mon Dec 12 17:15:48 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/lib/server/middlewareVFS/res.dirView/iconfont.ttf -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/lib/server/middlewareVFS/res.dirView/iconfont.woff -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const dirView = require('./res.dirView'); 3 | 4 | module.exports = dirView; 5 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.dirView/res.dirView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const f = require('jdf-utils').file; 5 | const logger = require('jdf-log'); 6 | 7 | const fileType = require('../../../VFS/fileType'); 8 | const env = require('../envConfig'); 9 | const fileRes = require('../res.file'); 10 | const dirHTML = require('./dir.html'); 11 | 12 | const view = module.exports = {}; 13 | 14 | let ignoreDir; 15 | let allowExtname; 16 | 17 | view.genHTML = function (projAbsDir) { 18 | // 扫描文件夹时只显示限定的文件和目录 19 | !ignoreDir && (ignoreDir = fileType.ignore.dir.join(',')); 20 | !allowExtname && (allowExtname = fileType.getPCExtname().join(',')); 21 | 22 | let data = { 23 | title: '', 24 | projectName: '', 25 | pathname: '', 26 | files: [], 27 | dirs: [] 28 | }; 29 | 30 | data.projectName = path.basename(env.webRoot); 31 | data.title = data.projectName + ' File List'; 32 | data.pathname = path.relative(env.webRoot, projAbsDir); 33 | data.pathname = data.pathname.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); 34 | 35 | // 返回上一级目录 36 | data.dirs.push({pathname: path.dirname(data.pathname), name: '..'}); 37 | 38 | fs.readdirSync(projAbsDir).forEach(name => { 39 | let absPath = path.join(projAbsDir, name); 40 | let pathname = path.relative(env.webRoot, absPath); 41 | 42 | // D:\\NodeApp\\jdfDev\\widget\\ -> (localhost:8080)/jdfDev/widget,用于浏览器地址栏,手动统一转为正斜杠 43 | pathname = pathname.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); 44 | logger.verbose(`file or dir relative path: ${pathname}`); 45 | 46 | // 过滤config.json 47 | if (pathname === 'config.json') { 48 | return; 49 | } 50 | 51 | // TODO 和VFS文件过滤统一 52 | // 过滤忽略目录 53 | if (f.isDir(absPath)) { 54 | let reg = new RegExp(`^${name}$|\\W${name}$|^${name}\\W|\\W${name}\\W`, 'i'); 55 | if (reg.test(ignoreDir)) { 56 | return; 57 | } 58 | 59 | data.dirs.push({pathname: pathname, name: name}); 60 | } else if (f.isFile(absPath)) { 61 | let extname = path.extname(absPath).slice(1); 62 | let reg = new RegExp(`^${extname}$|\\W${extname}$|^${extname}\\W|\\W${extname}\\W`, 'i'); 63 | 64 | // 过滤不符合的后缀 65 | if (!reg.test(allowExtname)) { 66 | return; 67 | } 68 | 69 | // 生成编译后文件路径 70 | let tExtname = fileRes.hasDiffTargetExtname(absPath); 71 | if (tExtname) { 72 | let oExtname = path.extname(absPath).slice(1); 73 | let reg = new RegExp(oExtname + '$', 'i'); 74 | pathname = pathname.replace(reg, tExtname); 75 | name = name.replace(reg, tExtname); 76 | data.files.push({pathname: pathname, name: name}); 77 | } else { 78 | data.files.push({pathname: pathname, name: name}); 79 | } 80 | } 81 | }); 82 | 83 | let html = dirHTML.html(data); 84 | return html; 85 | } 86 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/res.file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const f = require('jdf-utils').file; 6 | const VFS = require('../../VFS/VirtualFileSystem'); 7 | const fileType = require('../../VFS/fileType'); 8 | const injector = require('../injector'); 9 | const logger = require('jdf-log'); 10 | 11 | const fileRes = module.exports = {}; 12 | 13 | fileRes.getFileContent = function (absPath) { 14 | if (!absPath) { 15 | return undefined; 16 | } 17 | 18 | logger.verbose(`file abs path: ${absPath}`); 19 | 20 | let vfile = this.getTargetVFile(absPath); 21 | 22 | if (!vfile) { 23 | if (fileType.isPCExtname(absPath)) { 24 | try { 25 | let content = fs.readFileSync(absPath); 26 | return content; 27 | } catch (e) { 28 | logger.verbose(`no file exists: ${absPath}`); 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | // html注入代码 35 | let extname = path.extname(absPath); 36 | if (extname === '.html' || extname === '.xhtml' || extname === 'htm') { 37 | return injector.injectHTML(vfile.targetContent); 38 | } 39 | 40 | return vfile.targetContent; 41 | } 42 | 43 | // 在访问html文件的时候需要访问scss编译后的css文件 44 | // middlewareVFS中间件直接从VFS读取这些编译后的文件 45 | // 根目录 D:/NodeApp/jdfDev 46 | // 请求 http://localhost/widget/wname/wname.css 47 | // 则读取:D:/NodeApp/jdfDev/widget/wname/wname.css 48 | // 实际上只存在 D:/NodeApp/jdfDev/widget/wname/wname.scss 49 | // 根据以上信息获取到VFS中D:/NodeApp/jdfDev/widget/wname/wname.scss的targetContent 50 | fileRes.getTargetVFile = function (filepath) { 51 | let originpath = filepath; 52 | 53 | filepath = path.normalize(filepath); 54 | filepath = path.relative(VFS.originDir, filepath); 55 | filepath = path.join(VFS.targetDir, filepath); 56 | 57 | let vfile = VFS.queryFile(filepath, 'target'); 58 | 59 | if (vfile) { 60 | try { 61 | vfile.fetch(); 62 | } catch (e) { 63 | } 64 | return vfile; 65 | } 66 | 67 | vfile = VFS.queryFile(originpath); 68 | if (vfile) { 69 | try { 70 | vfile.fetch(); 71 | } catch (e) { 72 | } 73 | return vfile; 74 | } 75 | 76 | return null; 77 | } 78 | 79 | fileRes.hasDiffTargetExtname = function (filepath) { 80 | let vfile = VFS.queryFile(filepath); 81 | if (vfile && path.extname(vfile.originPath) !== path.extname(vfile.targetPath)) { 82 | return path.extname(vfile.targetPath).slice(1); 83 | } 84 | return false; 85 | } 86 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/tpl.footer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const footer = module.exports = {}; 4 | 5 | footer.genTPL = function () { 6 | let tpl = `

    work well!`; 7 | return tpl; 8 | } 9 | -------------------------------------------------------------------------------- /lib/server/middlewareVFS/view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const envConfig = require('./envConfig'); 4 | const dirView = require('./res.dirView'); 5 | const comboRes = require('./res.comboContent.js'); 6 | const notfound = require('./res.404.js'); 7 | const fileRes = require('./res.file.js'); 8 | 9 | const view = module.exports = {}; 10 | view.config = envConfig; 11 | 12 | view.dirView = dirView; 13 | view.comboRes = comboRes; 14 | view.res404 = notfound.res404; 15 | view.fileRes = fileRes; 16 | -------------------------------------------------------------------------------- /lib/server/mime.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "css": "text/css", 3 | "gif": "image/gif", 4 | "html": "text/html", 5 | "tpl": "text/html", 6 | "vm": "text/html", 7 | "shtml": "text/html", 8 | "ico": "image/x-icon", 9 | "jpeg": "image/jpeg", 10 | "jpg": "image/jpeg", 11 | "js": "application/javascript", 12 | "json": "application/json", 13 | "pdf": "application/pdf", 14 | "png": "image/png", 15 | "svg": "image/svg+xml", 16 | "swf": "application/x-shockwave-flash", 17 | "tiff": "image/tiff", 18 | "txt": "text/plain", 19 | "wav": "audio/x-wav", 20 | "wma": "audio/x-ms-wma", 21 | "wmv": "video/x-ms-wmv", 22 | "xml": "text/xml", 23 | "ttf":"font/ttf", 24 | "otf":"font/opentype", 25 | "woff":"application/font-woff", 26 | "woff2":"application/font-woff2", 27 | "eot":"application/vnd.ms-fontobject" 28 | }; -------------------------------------------------------------------------------- /lib/server/openurl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @openurl 5 | * @from https://github.com/rauschma/openurl/blob/master/openurl.js 6 | * @example require("./openurl").open(url); 7 | */ 8 | const spawn = require('child_process').spawn; 9 | let command; 10 | 11 | switch(process.platform) { 12 | case 'darwin': 13 | command = 'open'; 14 | break; 15 | case 'win32': 16 | command = 'explorer.exe'; 17 | break; 18 | case 'linux': 19 | command = 'xdg-open'; 20 | break; 21 | default: 22 | throw new Error('Unsupported platform: ' + process.platform); 23 | } 24 | 25 | /** 26 | * Error handling is deliberately minimal, as this function is to be easy to use for shell scripting 27 | * 28 | * @param url The URL to open 29 | * @param callback A function with a single error argument. Optional. 30 | */ 31 | 32 | function open(url, callback) { 33 | const child = spawn(command, [url]); 34 | let errorText = ''; 35 | child.stderr.setEncoding('utf8'); 36 | child.stderr.on('data', function (data) { 37 | errorText += data; 38 | }); 39 | child.stderr.on('end', function () { 40 | if (errorText.length > 0) { 41 | let error = new Error(errorText); 42 | if (callback) { 43 | callback(error); 44 | } else { 45 | throw error; 46 | } 47 | } else if (callback) { 48 | callback(error); 49 | } 50 | }); 51 | } 52 | 53 | module.exports.open = open; 54 | -------------------------------------------------------------------------------- /lib/vm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const util = require('util'); 6 | const jdfUtils = require('jdf-utils'); 7 | const logger = require('jdf-log'); 8 | const $ = jdfUtils.base; 9 | const f = jdfUtils.file; 10 | const jdf = require("./jdf"); 11 | const velocity = require('velocityjs'); 12 | const VFS = require('./VFS/VirtualFileSystem'); 13 | const vm = module.exports; 14 | 15 | /** 16 | * @rander data 17 | * 格式: 18 | * 19 | * 20 | * 21 | * vm's content 22 | * 23 | * @{String} vmSource vm内容 24 | * @{Object} dataObj vm对应的数据 25 | * @{String} dirname vm的dirname 26 | */ 27 | vm.render = function(vmSource, options) { 28 | let dataObj = options.dataObj; 29 | let dirname = options.dirname; 30 | let existMap = options.existMap; 31 | 32 | if (!(vmSource && dataObj)) { 33 | return ''; 34 | } 35 | 36 | let macros = { 37 | parse: function (name) { 38 | let content = `\n`; 39 | let vmpath = path.resolve(dirname, name); 40 | let vmVfile = VFS.queryFile(vmpath); 41 | if (!vmVfile) { 42 | logger.error(`'#parse(${name})' in ${dirname}'s vm, path not exist or not a file`); 43 | return name; 44 | } 45 | 46 | // TODO content不将js css放进去,只放到cssMap,jsMap中 47 | 48 | // 添加#parse(vmpath)相应的css和js标签 49 | let vmName = path.basename(vmpath).replace(path.extname(vmpath), ''); 50 | if (!existMap.cssMap[vmName]) { 51 | // 获取vfile,没有就不添加css标签到html中 52 | let csspath = vmpath.replace(path.extname(vmpath), '.css'); 53 | csspath = path.relative(VFS.originDir, csspath); 54 | csspath = path.join(VFS.targetDir, csspath); 55 | let cssVfile = VFS.queryFile(csspath, 'target'); 56 | if (cssVfile) { 57 | existMap.cssMap.set(vmName, true); 58 | } 59 | } 60 | if (!existMap.jsMap[vmName]) { 61 | let jspath = vmpath.replace(path.extname(vmpath), '.js'); 62 | jspath = path.relative(VFS.originDir, jspath); 63 | jspath = path.join(VFS.targetDir, jspath); 64 | let jsVfile = VFS.queryFile(jspath, 'target'); 65 | if (jsVfile) { 66 | existMap.jsMap.set(vmName, true); 67 | } 68 | } 69 | // 递归解析 70 | content += vm.render(vmVfile.targetContent, { 71 | dataObj: dataObj, 72 | dirname: path.dirname(vmpath), 73 | existMap: existMap 74 | }); 75 | 76 | content += ``; 77 | 78 | return content; 79 | } 80 | } 81 | 82 | return velocity.render(vmSource, dataObj, macros); 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jdfx", 3 | "description": "京东前端自动化构建工具", 4 | "version": "3.4.13", 5 | "author": { 6 | "name": "jdfx", 7 | "email": "chenxiaochun@jd.com" 8 | }, 9 | "homepage": "https://github.com/jdf2e/jdf", 10 | "keywords": [ 11 | "jdf", 12 | "jdfx", 13 | "build tools", 14 | "构建工具", 15 | "tool", 16 | "cli", 17 | "front-end", 18 | "webpack", 19 | "fe" 20 | ], 21 | "license": "MIT", 22 | "bin": { 23 | "jdf": "bin/jdf" 24 | }, 25 | "engines": { 26 | "node": ">= 4.4.5" 27 | }, 28 | "main": "index.js", 29 | "scripts": { 30 | "test": "mocha test/index.js" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/jdf2e/jdf.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/jdf2e/jdf/issues" 38 | }, 39 | "preferGlobal": true, 40 | "readmeFilename": "README.md", 41 | "dependencies": { 42 | "acorn": "^4.0.4", 43 | "atropa-jslint": "0.1.2", 44 | "autoprefixer": "^6.5.2", 45 | "axios": "^0.17.0", 46 | "babel-core": "^6.18.0", 47 | "babel-preset-env": "^1.7.0", 48 | "browser-sync": "2.21.0", 49 | "bs-html-injector": "^3.0.3", 50 | "cheerio": "^0.22.0", 51 | "clean-css": "2.1.8", 52 | "commander": "^2.9.0", 53 | "csslint": "0.10.0", 54 | "escape-string-regexp": "^1.0.5", 55 | "escodegen": "^1.8.1", 56 | "fs-extra": "^1.0.0", 57 | "glob": "^7.1.1", 58 | "html-minifier": "0.6.9", 59 | "htmllint": "0.0.7", 60 | "iconv-lite": "^0.4.10", 61 | "jdf-css-sprite": "^1.1.6", 62 | "jdf-img-minify": "0.1.0", 63 | "jdf-log": "^0.0.4", 64 | "jdf-upload": "^0.2.0", 65 | "jdf-utils": "^1.1.3", 66 | "js-beautify": "1.5.4", 67 | "jsmart": "^2.14.0", 68 | "less": "^2.7.1", 69 | "lodash": "^4.17.2", 70 | "minimatch": "^3.0.3", 71 | "node-sass": "^4.14.1", 72 | "node-watch": "^0.5.3", 73 | "postcss": "^5.2.5", 74 | "shelljs": "^0.7.5", 75 | "socket.io": "^2.0.4", 76 | "socket.io-client": "^2.0.4", 77 | "strip-json-comments": "^2.0.1", 78 | "uglify-es": "^3.1.3", 79 | "uglify-js": "2.6.0", 80 | "uuid": "^3.0.0", 81 | "velocityjs": "0.4.3" 82 | }, 83 | "devDependencies": { 84 | "expect.js": "^0.3.1", 85 | "mocha": "^3.2.0" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /template/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "192.168.181.73", 3 | "user": "arch", 4 | "password": "arch", 5 | "projectPath": "jdf_init", // 默认输出项目名称 6 | "compressThreadCrisis": 200, // 工程文件超过200个时开启多线程压缩 7 | "cdn": "//misc.360buyimg.com", // cdn前缀,如果不需要cdn,置成空字符串 8 | "output": { 9 | "cssCombo": true, // css进行combo 10 | "jsCombo": true, // js进行combo 11 | 12 | "compressJs": true, // 是否开启压缩js文件 13 | "compressCss": true, // 是否开启压缩css文件 14 | "compressImage": true, // 是否开启压缩图片 15 | 16 | "excludeFiles": "" // 不需要输出的文件/文件夹,路径相对于当前项目根目录,以逗号分隔,例如:"test,test.css" 17 | }, 18 | "plugins": [] // 引用的插件 19 | } -------------------------------------------------------------------------------- /template/css/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/template/css/style.scss -------------------------------------------------------------------------------- /template/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 51 | 52 |

    53 |

    jdf 快速入门

    54 |

    2016-12-12

    55 |

    安装使用

    56 | 57 |

    jdf命令行工具是jdf前端集成解决方案的工具集,基于node.js和npm,jdf前需要先安装node.js,然后通过npm命令安装jdfx

    58 | 59 |
    npm install jdfx -g
    60 | 61 |

    安装测试,执行如下命令,如果出现版本号则说明你已安装成功

    62 | 63 |
    jdf -V
    64 | 65 |

    更新jdf

    66 | 67 |
    npm update jdfx -g
    68 | 69 |

    新建项目目录

    70 | 71 |

    在本机svn目录,主干上新建项目,比如product下项目test,即svn目录为/product/test
    注:product为大项目名,test为测试项目

    72 | 73 |

    初始化项目

    74 | 75 |

    从命令行下进入test文件夹,在命令行执行如下命令,生成标准化的项目文件夹

    76 | 77 |
    jdf init [projectName]
    78 | 79 |

    test文件中里包括css、html、js、widget、config.json,其中config.json为配置文件,所有配置都需要修改此文件

    80 | 81 |

    注1:json文件不允许有注释,单双引号使用时也需要统一
    注2:windows7下在当前文件夹打开命令行的方法:Shift+鼠标右键,选择"在此处打开命令窗口"
    注3:windows下CMD路径常用操作如下

    82 | 83 |

    项目开发

    84 | 85 |

    在html,js,css等文件夹中新建相应文件
    在widget文件夹新建抽离规划好的widget模块
    在当前项目中,新建widget的命令为jdf widget -create xxx

    86 | 87 |

    本地预览调试

    88 | 89 |

    可以通过jdf build 在浏览器中查看构建后的当前工程,包括less,sass编译,widget编译等

    90 | 91 |
    jdf build
    92 | 93 |

    注意

    94 | 95 |
      96 |
    • 本地服务器默认启用80端口,如果此端口被占用,工具会自动启动一个新的端口:例如8080
    • 97 |
    • 命令执行后工具本地服务器会一直运行,可以使用ctrl+c命令退出本地server
    • 98 |
    • 如果发现本地服务器在浏览器输出的文件夹和本地项目不一致,可通过jdf clean,清除服务器后台缓存,强制同步一次
    • 99 |
    100 | 101 |

    本地调试服务器启动成功后,可能通过jdf build -o命令自动打开浏览器,也可以复制http://localhost/在浏览上打开,jdf内置了browserSync,会自动同步刷新页面

    102 | 103 |

    项目输出

    104 | 105 |

    jdf output输出项目文件夹,包括压缩合并后的css,js,images,静态资源加cdn前缀,同时压缩所有图片
    jdf output js/test.js css 自定义输出自己需要的文件或文件夹

    106 | 107 |

    项目联调和发布

    108 | 109 |

    jdf upload 发布至远端机器,主要包括css/js/widget,供产品,设计师查看效果,以及后端工程师联调
    110 | 111 |

    更多信息更参考:

    112 |

    113 | jdfx官方地址 114 |

    115 |
    116 | 117 |
    118 |
    119 | 120 | 121 | -------------------------------------------------------------------------------- /test/buildOutputWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const expect = require('expect.js'); 6 | const logger = require('jdf-log'); 7 | 8 | const VFS = require('../lib/VFS/VirtualFileSystem'); 9 | const buildOutputWidget = require('../lib/buildOutputWidget'); 10 | const widgetParser = require('../lib/buildWidget'); 11 | 12 | describe('测试widgetOutputName', function () { 13 | logger.level(-1); 14 | describe('查找并解析{%widgetOutputName %}标签', function () { 15 | it('单行注释标签返回{}', function () { 16 | let comment1 = ''; 17 | let info = widgetParser.parseOutputName(comment1); 18 | expect(info).to.eql({}); 19 | 20 | let comment2 = ``; 21 | let info2 = widgetParser.parseOutputName(comment2); 22 | expect(info2).to.eql({}); 23 | }); 24 | it('多行注释标签返回{}', function () { 25 | let comment1 = ``; 29 | let info = widgetParser.parseOutputName(comment1); 30 | expect(info).to.eql({}); 31 | 32 | let comment2 = ``; 36 | let info2 = widgetParser.parseOutputName(comment2); 37 | expect(info2).to.eql({}); 38 | }); 39 | it('widgetOutputName标签解析', function () { 40 | let comment = `{%widgetOutputName="name1"%}`; 41 | let info2 = widgetParser.parseOutputName(comment); 42 | expect(info2.name).to.equal('name1'); 43 | }); 44 | it('注释非注释写在一起', function () { 45 | let comment = `ap

    aaabwww.jd.com

    46 | {%widgetOutputName="name1"%} 47 | 48 |
    ad
    49 | {%widgetOutputName="name3"%} 50 | `; 51 | let info2 = widgetParser.parseOutputName(comment); 52 | expect(info2.name).to.equal('name1'); 53 | }); 54 | it('一个html文档', function () { 55 | let html = ` 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
    write in html, yes changed....
    65 | 69 | {%widgetOutputName='output' %} 70 | 71 | 72 | `; 73 | let info = widgetParser.parseOutputName(html); 74 | expect(info.name).to.equal('output'); 75 | 76 | }); 77 | }); 78 | 79 | describe('分别合并widget的css,js到一个文件', function () { 80 | let info = { name: 'output', 81 | text: '{%widgetOutputName="output" %}', 82 | concatTag: { js: true, css: true } 83 | }; 84 | it('delete 87 | ` 88 | } 89 | buildOutputWidget.delWidgetTagInHTML(info, htmlVfile); 90 | expect(htmlVfile.targetContent.replace(/\W*/g, '')).to.equal(''); 91 | 92 | let htmlVfile1 = { 93 | targetContent: ` 94 | 95 | ` 96 | } 97 | buildOutputWidget.delWidgetTagInHTML(info, htmlVfile1); 98 | expect(htmlVfile1.targetContent).to.contain('./es6.js'); 99 | }); 100 | it('delete ', function () { 101 | let htmlVfile = { 102 | targetContent: ` 103 | ` 104 | } 105 | buildOutputWidget.delWidgetTagInHTML(info, htmlVfile); 106 | expect(htmlVfile.targetContent.replace(/\W*/g, '')).to.equal(''); 107 | 108 | let htmlVfile1 = { 109 | targetContent: ` 110 | 111 | ` 112 | } 113 | buildOutputWidget.delWidgetTagInHTML(info, htmlVfile1); 114 | expect(htmlVfile1.targetContent).to.contain('./css1.css'); 115 | }); 116 | it('the css case ./i/a.png', function () { 117 | let vfile = { 118 | originPath: 'D:/TMP/bugfix/widget/a/a.css', 119 | targetContent: `.a {background: url(./i/a.png) no-repeat;}` 120 | }; 121 | let targetDir = 'D:/TMP/bugfix/css'; 122 | let transferContent = buildOutputWidget.cssPathRelative(vfile, targetDir); 123 | expect(transferContent).equal(`.a {background: url(../widget/a/i/a.png) no-repeat;}`); 124 | }); 125 | it('the css case /i/a.png', function () { 126 | let vfile = { 127 | originPath: 'D:/TMP/bugfix/widget/a/a.css', 128 | targetContent: `.a {background: url(/i/a.png) no-repeat;}` 129 | }; 130 | let targetDir = 'D:/TMP/bugfix/css'; 131 | let transferContent = buildOutputWidget.cssPathRelative(vfile, targetDir); 132 | expect(transferContent).equal(`.a {background: url(/i/a.png) no-repeat;}`); 133 | }); 134 | it('the css case "../b/i/b.png"', function () { 135 | let vfile = { 136 | originPath: 'D:/TMP/bugfix/widget/a/a.css', 137 | targetContent: `.a {background: url("../b/i/b.png") no-repeat;}` 138 | }; 139 | let targetDir = 'D:/TMP/bugfix/css'; 140 | let transferContent = buildOutputWidget.cssPathRelative(vfile, targetDir); 141 | expect(transferContent).equal(`.a {background: url(../widget/b/i/b.png) no-repeat;}`); 142 | }); 143 | it('the css case //www.jd.com/b/i/b.png', function () { 144 | let vfile = { 145 | originPath: 'D:/TMP/bugfix/widget/a/a.css', 146 | targetContent: `.a {background: url(//www.jd.com/b/i/b.png) no-repeat;}` 147 | }; 148 | let targetDir = 'D:/TMP/bugfix/css'; 149 | let transferContent = buildOutputWidget.cssPathRelative(vfile, targetDir); 150 | expect(transferContent).equal(`.a {background: url(//www.jd.com/b/i/b.png) no-repeat;}`); 151 | }); 152 | it('the js case all', function () { 153 | let vfile = { 154 | originPath: 'D:/TMP/bugfix/widget/a/a.js', 155 | targetContent: `define('b', [], function(require, module, exports){var a = require('/widget/a/a.js');seajs.use('../a/a.js');});` 156 | }; 157 | let targetDir = 'D:/TMP/bugfix/js'; 158 | let transferContent = buildOutputWidget.jsPathRelative(vfile, targetDir); 159 | expect(transferContent).equal(`define('b', ['/widget/a/a.js'], function (require, module, exports) { 160 | var a = require('/widget/a/a.js'); 161 | seajs.use('../widget/a/a.js'); 162 | });`); 163 | }); 164 | }); 165 | 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /test/buildWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | const logger = require('jdf-log'); 5 | const escapeStringRegexp = require('escape-string-regexp'); 6 | 7 | const buildWidget = require('../lib/buildWidget'); 8 | 9 | let html = ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {%widget name="nav" %} 22 | {%widget data='{"portal_floor_id": 0}' 23 | floorname="运营商-菜单" 24 | name="menu" 25 | cmsdata='{"floorclass": "floor-201707311537"}'%} 26 | {%widget name="slide" %} 27 | {%widget name="slide"%} 28 | {%widget name="slide"%} 29 | 30 | `; 31 | 32 | describe('测试buildWidget', function () { 33 | logger.level(-1); 34 | let widgetList = buildWidget.parseWidget(html); 35 | describe('查找并解析{%widget %}标签', function () { 36 | it('共有5个widget', function () { 37 | expect(widgetList.length).to.eql(5); 38 | }); 39 | it('widget写在单行', function () { 40 | expect(widgetList[0].name).to.equal('nav'); 41 | expect(widgetList[2].name).to.equal('slide'); 42 | expect(widgetList[3].name).to.equal('slide'); 43 | expect(widgetList[4].name).to.equal('slide'); 44 | }); 45 | it('widget属性分多行写', function () { 46 | expect(widgetList[1].name).to.equal('menu'); 47 | expect(JSON.parse(widgetList[1].data).portal_floor_id).to.equal(0); 48 | expect(widgetList[1].text).to.equal(`{%widget data='{"portal_floor_id": 0}' 49 | floorname="运营商-菜单" 50 | name="menu" 51 | cmsdata='{"floorclass": "floor-201707311537"}'%}`); 52 | }); 53 | it('widget相关文件名的正则检测', function () { 54 | let widgetInfo = { 55 | name: 'test.a' 56 | } 57 | let oBasename = 'test.a.a.js'; 58 | let oBasename1 = 'test.a.js'; 59 | let widgetNameReg = new RegExp(escapeStringRegexp(widgetInfo.name) + '\.\\w+$'); 60 | expect(widgetNameReg.test(oBasename)).to.equal(false); 61 | expect(widgetNameReg.test(oBasename1)).to.equal(true); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/buildcss.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const expect = require('expect.js'); 6 | 7 | const VFS = require('../lib/VFS/VirtualFileSystem'); 8 | const buildCss = require('../lib/buildCss'); 9 | 10 | describe('测试编译css', function () { 11 | let cssfileDir = path.join(process.cwd(), 'test/vfs/files'); 12 | describe('buildcss in VFS', function () { 13 | it('#handleLess', function (done) { 14 | let filename = path.join(cssfileDir, 'less.less'); 15 | let vfile = VFS.queryFile(filename); 16 | let tContent = vfile.targetContent; 17 | buildCss.handleLess(vfile).then(function () { 18 | expect(tContent).not.to.equal(vfile.targetContent); 19 | done(); 20 | }); 21 | }); 22 | it('#handleSass', function (done) { 23 | let filename = path.join(cssfileDir, 'sass.scss'); 24 | let vfile = VFS.queryFile(filename); 25 | let tContent = vfile.targetContent; 26 | buildCss.handleSass(vfile).then(function () { 27 | expect(tContent).not.to.equal(vfile.targetContent); 28 | done(); 29 | }); 30 | }); 31 | it('#postCSSProcess', function (done) { 32 | let filename = path.join(cssfileDir, 'css.css'); 33 | let vfile = VFS.queryFile(filename); 34 | let tContent = vfile.targetContent; 35 | buildCss.postCSSProcess(vfile).then(function () { 36 | expect(tContent).not.to.equal(vfile.targetContent); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "ftpServerIp", 3 | "user": "anonymous", 4 | "password": "anonymous", 5 | "projectPath": "test/1.0.1", 6 | "output": { 7 | "cssAutoPrefixer": false, 8 | "excludeFiles": ["psd", "doc"] 9 | } 10 | } -------------------------------------------------------------------------------- /test/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const expect = require('expect.js'); 6 | 7 | const jdf = require('../../lib/jdf.js'); 8 | const config = jdf.config; 9 | 10 | describe('读取配置文件', function () { 11 | let npmRoot = process.cwd(); 12 | 13 | let userConfigPath = path.join(npmRoot, 'test/config/config.json'); 14 | let userConfig = jdf.getUserConfig(userConfigPath); 15 | describe('jdf.getUserConfig()', function () { 16 | it('user projectPath in config.json should be test/1.0.1', function () { 17 | expect(userConfig.projectPath).to.equal('test/1.0.1'); 18 | }); 19 | }); 20 | 21 | describe('jdf.mergeConfig()', function () { 22 | let env = path.join(npmRoot, 'test/config'); 23 | process.chdir(env); 24 | jdf.init(); 25 | let mergeConfig = jdf.mergeConfig(); 26 | it('projectPath should be test/1.0.1', function () { 27 | expect(mergeConfig.projectPath).to.equal('test/1.0.1'); 28 | }); 29 | it('excludeFiles should be ["psd", "doc"]', function () { 30 | expect(mergeConfig.output.excludeFiles.length).to.equal(2); 31 | expect(mergeConfig.output.excludeFiles).to.eql(['psd', 'doc']); 32 | }); 33 | process.chdir(npmRoot); 34 | }); 35 | 36 | describe('jdf.init()', function () { 37 | let env = path.join(npmRoot, 'test/config'); 38 | process.chdir(env); 39 | let mergeConfig = jdf.init(); 40 | it('projectPath should be test/1.0.1', function () { 41 | expect(mergeConfig.projectPath).to.equal('test/1.0.1'); 42 | }); 43 | it('excludeFiles should be ["psd", "doc"]', function () { 44 | expect(mergeConfig.output.excludeFiles.length).to.equal(2); 45 | expect(mergeConfig.output.excludeFiles).to.eql(['psd', 'doc']); 46 | }); 47 | it('jdf.currentDir should be ./test/config', function () { 48 | expect(jdf.currentDir.replace(/\\/g, '/')).to.match(/test\/config$/); 49 | }); 50 | it('jdf.transferDir should be os.tmpdir() join jdf-temp/project/config', function () { 51 | expect(jdf.transferDir.replace(/\\/g, '/')).to.match(/\.jdf-temp\/project\/config$/); 52 | }); 53 | it('jdf.outputDir should be relative to currentDir: build/test/1.0.1', function () { 54 | let relativePath = path.relative(jdf.currentDir, jdf.outputDir); 55 | expect(relativePath.replace(/\\/g, '/')).to.equal('build/test/1.0.1'); 56 | }); 57 | process.chdir(npmRoot); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const expect = require('expect.js'); 5 | 6 | require('./vfs/virtual-file'); 7 | 8 | const VFS = require('../lib/VFS/VirtualFileSystem'); 9 | // npm run test 10 | const testDir = path.join(process.cwd(), './test'); 11 | VFS.setOriginDir(testDir); 12 | VFS.setTargetDir(path.join(testDir, './build')); 13 | VFS.addIgnore('build', 'dir'); 14 | VFS.readFilesInOriginDirSync(); 15 | 16 | describe('加载文件到VFS', function () { 17 | it('read files success', function () { 18 | expect(VFS.fileList.length).to.be.above(0); 19 | }); 20 | }); 21 | 22 | require('./config'); 23 | 24 | require('./buildcss'); 25 | 26 | require('./urlReplace'); 27 | 28 | require('./buildWidget'); 29 | 30 | require('./buildOutputWidget'); 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/urlReplace/comboUrlPath/case01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jdf-test 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/urlReplace/comboUrlPath/result01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jdf-test 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/vfs/files/QR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/test/vfs/files/QR.jpg -------------------------------------------------------------------------------- /test/vfs/files/css.css: -------------------------------------------------------------------------------- 1 | .css { 2 | display: flex; 3 | } -------------------------------------------------------------------------------- /test/vfs/files/doc.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf2e/jdf/8ef48da64ca10305ee47e8c0aff6e23a3d647c37/test/vfs/files/doc.docx -------------------------------------------------------------------------------- /test/vfs/files/es6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let vars = 'convert by Babel'; 4 | -------------------------------------------------------------------------------- /test/vfs/files/importless.less: -------------------------------------------------------------------------------- 1 | .less { 2 | .import { 3 | color: #ddd; 4 | } 5 | } -------------------------------------------------------------------------------- /test/vfs/files/js.js: -------------------------------------------------------------------------------- 1 | console.log('read by VFS'); 2 | -------------------------------------------------------------------------------- /test/vfs/files/less.less: -------------------------------------------------------------------------------- 1 | @import "importless.less"; 2 | @import "./css.css"; 3 | .le { 4 | .ss { 5 | @color: #ccc; 6 | &:before { 7 | content: 'less read by VFS'; 8 | color: @color; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/vfs/files/sass.scss: -------------------------------------------------------------------------------- 1 | @import "./css.css"; 2 | .sa { 3 | $color: #ddd; 4 | .ss { 5 | &:before { 6 | content: 'sass read by VFS'; 7 | color: $color; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/vfs/files/text.txt: -------------------------------------------------------------------------------- 1 | 这个文件不会被VFS读取。 2 | This file will not be read by VFS. -------------------------------------------------------------------------------- /test/vfs/virtual-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | const expect = require('expect.js'); 7 | const esr = require('escape-string-regexp'); 8 | 9 | const VFile = require('../../lib/VFS/VirtualFile'); 10 | 11 | describe('测试VFile', function () { 12 | describe('must be absolute path', function () { 13 | let relativePath = './files/css.css'; 14 | it(`should return {}: ${relativePath} `, function () { 15 | let vfile = new VFile(relativePath); 16 | expect(vfile).to.eql({}); 17 | }); 18 | let absPath = path.join(process.cwd(), './test/vfs', relativePath); 19 | it(`should success: ${absPath}`, function () { 20 | let vfile = new VFile(absPath); 21 | expect(vfile.originContent.replace(new RegExp(esr(os.EOL), 'g'), '\n')).to.equal('.css {\n display: flex;\n}'); 22 | }); 23 | }); 24 | }); 25 | --------------------------------------------------------------------------------