├── .editorconfig
├── .gitignore
├── README.md
├── bin
└── wecos.js
├── index.js
├── lib
├── common.js
├── compress.js
├── config.js
├── task.js
├── upload.js
└── watcher.js
├── package.json
└── test
├── index.js
├── test-config-file.js
├── test-config-option.js
├── test-task.js
├── test-upload.js
└── test.jpg
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_style = space
9 | indent_size = 4
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | test/app-src
4 | test/app
5 | test/.tmp
6 | test/.backup
7 | test/wecos_backup
8 | test/wecos.config.json
9 | npm-debug.log
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeCOS —— 微信小程序 COS 瘦身解决方案
2 |
3 | 通过WeCOS,你的小程序项目中的图片资源会自动上传到你的[腾讯云对象存储服务(COS)](https://www.qcloud.com/product/cos),且WeCOS自动替换代码中图片资源地址的引用为线上地址,移除项目目录中的图片资源,从而减小小程序包大小,为你解决包大小超过限制的烦恼。
4 | 
5 |
6 |
7 | ## 为什么你需要 WeCOS
8 |
9 | 为了提升小程序体验流畅度,编译后的代码包大小需小于 1MB ,大于 1MB 的代码包将上传失败。
10 |
11 | 在开发小程序的过程中,图片资源通常会占用较大空间,很容易超出官方的1MB限制。这时候,使用WeCOS,可以让你在开发过程中不需要关心图片资源占用多少空间的问题,专注于自己的逻辑开发。
12 |
13 |
14 | ## 准备工作
15 | * 进入[腾讯云官网](https://www.qcloud.com),注册帐号
16 | * 登录[云对象存储服务(COS)控制台](https://console.qcloud.com/cos4),开通COS服务,创建Bucket
17 | * 安装[Node.js](https://nodejs.org)环境
18 |
19 |
20 | ## 安装
21 |
22 | ```js
23 | npm install -g wecos
24 | ```
25 |
26 |
27 | ## 基本配置
28 | 在你的小程序目录同级下创建`wecos.config.json`文件
29 |
30 | `wecos.config.json`配置项例子:
31 | ```json
32 | {
33 | "appDir": "./app",
34 | "cos": {
35 | "secret_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
36 | "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
37 | "bucket": "wxapp-1251902136",
38 | "region": "ap-guangzhou", //创建bucket时选择的地域简称
39 | "folder": "/", //资源存放在bucket的哪个目录下
40 | }
41 | }
42 | ```
43 |
44 | | 配置项 | 类型 | 说明 |
45 | |:-- |:-- |:-- |
46 | | appDir | **[String]** | 默认 `./app`,小程序项目目录 |
47 | | cos | **[Object]** | 必填,填写需要上传到COS对应的配置信息,部分信息可在[COS控制台](https://console.qcloud.com/cos4/secret)查看 |
48 |
49 |
50 | ## 使用
51 |
52 | 在配置文件同级目录下命令行执行
53 | ```js
54 | wecos
55 | ```
56 | 注意,执行前需要在该目录下创建`wecos.config.json`文件
57 |
58 |
59 |
60 |
61 | ## 高级配置
62 |
63 | | 配置项 | 类型 | 说明 |
64 | |:-- |:-- |:-- |
65 | | backupDir | **[String]** | 默认 `./wecos_backup`,备份目录 |
66 | | uploadFileSuffix | **[Array]** | 默认 `[".jpg", ".png", ".gif"]`,图片上传后缀名配置 |
67 | | uploadFileBlackList | **[Array]** | 默认 `[]`,图片资源黑名单 |
68 | | replaceHost | **[String]** | 默认 `''`,把指定域名替换成 targetHost |
69 | | targetHost | **[String]** | 默认 `''`,使用自定义域名 |
70 | | compress | **[Boolean]** | 默认 `false`,是否开启压缩图片 |
71 | | watch | **[Boolean]** | 默认 `true`,是否开启实时监听项目目录 |
72 |
73 | #### 设置备份目录
74 |
75 | 由于WeCOS在运行时会自动将项目下的图片上传至COS然后删除,这样可能存在丢失源文件的风险,因此我们也提供了备份源文件的功能,每上传一张图片,会在项目同级的某个目录下备份该文件
76 |
77 | 为了方便使用,可以通过以下配置来修改备份目录名,如果不需要使用该功能,可以设置为空值
78 | ```json
79 | "backupDir": "./wecos_backup"
80 | ```
81 |
82 |
83 | #### 设置图片后缀
84 |
85 | 有些时候,我们需要限制上传图片的格式,例如只允许`jpg`格式,可以通过WeCOS提供的图片后缀配置项来定义
86 |
87 | WeCOS默认支持`jpg,png,gif`三种格式,假如你还需要添加其他格式,例如webp,可以在该配置项中添加
88 |
89 | ```json
90 | "uploadFileSuffix": [".jpg",".png",".gif",".webp"]
91 | ```
92 |
93 |
94 | #### 设置图片黑名单
95 |
96 | 开发过程中,某些特定的图片我们不希望被上传,可以通过WeCOS的黑名单配置来解决这个问题,配置后上传程序会自动忽略掉这些图片
97 |
98 | 黑名单配置支持目录或具体到文件名的写法
99 | ```json
100 | "uploadFileBlackList": ["./images/logo.png","./logo/"]
101 | ```
102 |
103 |
104 | #### 自定义域名
105 |
106 | 如果希望 COS 文件链接使用自定义的域名,可以配置 targetHost 代替默认域名,可以省略:`http://`:
107 |
108 | ```json
109 | "targetHost": "http://example.com"
110 | ```
111 |
112 | 如果代码中的图片链接想换一个域名,可以配置 replaceHost targetHost 来实现。
113 |
114 | ```json
115 | "replaceHost": "http://wx-12345678.myqcloud.com",
116 | "targetHost": "https://example.com"
117 | ```
118 |
119 |
120 | #### 开启图片压缩
121 |
122 | 图片上传到COS之后虽然大大减轻了程序包的大小,但如果图片自身体积过大,访问速度也会影响到用户体验
123 |
124 | 令人激动的是,WeCOS在图片上云的基础功能上还额外提供了基于[腾讯云万象优图](https://www.qcloud.com/product/ci)的图片压缩功能。
125 |
126 | 首先,你需要在[万象优图控制台](https://console.qcloud.com/ci)创建 COS的同名bucket
127 |
128 | 然后,开启该选项,资源将被压缩后上传(注:如果图片已经小到一定程度,压缩后大小可能不会变化)
129 |
130 | ```json
131 | "compress": true
132 | ```
133 |
134 |
135 | #### 设置实时监听
136 |
137 | WeCOS默认实时监听项目目录变化,自动处理图片资源,在开发过程中,如果觉得实时监听不方便,或者只需要一次性处理就停止,可以修改该配置,程序将只会执行一次后退出
138 | ```json
139 | "watch": false
140 | ```
141 |
142 |
143 |
144 |
145 | ## 高级用法
146 | 如果你除了上述使用命令行来执行的方式外,还想使用其他的方式,例如定制化成自己的模块,我们也提供了直接引用的使用方法满足你个性化的需求
147 |
148 | ```js
149 | var wecos = require('wecos');
150 |
151 | /**
152 | * option 可选 [String|Object]
153 | * 传入 String,指定配置文件路径
154 | * 传入 Object,指定配置项
155 | */
156 | wecos([option]);
157 |
158 | //指定配置文件路径
159 | wecos('./wecos-config.js');
160 |
161 | //指定配置项
162 | wecos({
163 | appDir: './xxx',
164 | cos: {
165 | ...
166 | }
167 | });
168 |
169 | ```
170 |
171 |
172 |
173 |
174 | ## 相关
175 |
176 | * [WeCOS-UGC-DEMO](https://github.com/tencentyun/wecos-ugc-upload-demo)——小程序用户资源上传COS DEMO
177 |
178 | * [COS-AUTH](https://github.com/tencentyun/cos-auth)——COS鉴权服务器DEMO
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/bin/wecos.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | var pkg = require('../package.json');
5 | var hasArg = false;
6 | process.argv.slice(1).filter(function (arg) {
7 | var match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=(.*))?$/i);
8 | if (match) {
9 | arg = match[1];
10 | } else {
11 | return arg;
12 | }
13 |
14 | switch (arg) {
15 | case 'v':
16 | case 'version':
17 | hasArg = true;
18 | console.log('wecos ' + pkg.version);
19 | break;
20 | }
21 | });
22 |
23 | if (!hasArg) {
24 | var watchTask = require('../lib/watcher');
25 | watchTask();
26 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var watchTask = require('./lib/watcher');
4 |
5 | module.exports = watchTask;
--------------------------------------------------------------------------------
/lib/common.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var fs = require('fs');
4 | var globalConfig = require('./config');
5 | var pkg = require('../package');
6 | var COS = require('cos-nodejs-sdk-v5');
7 |
8 | var tool = {
9 | uploadFile: uploadFile
10 | };
11 |
12 | var cos = new COS({
13 | UserAgent: 'cos-wecos-' + pkg.version,
14 | });
15 | function uploadFile(_config, fromPath, toPath, cb) {
16 | cos.options.SecretId = globalConfig.cos.secret_id;
17 | cos.options.SecretKey = globalConfig.cos.secret_key;
18 |
19 | // 桶里的文件夹
20 | var cosFolder = globalConfig.cos.folder || '/';
21 | if (cosFolder.substr(-1, 1) !== '/') {
22 | cosFolder = cosFolder + '/'
23 | }
24 |
25 | var opt = {
26 | Bucket: globalConfig.cos.bucket || (globalConfig.cos.bucketname + '-' + globalConfig.cos.appid),
27 | Region: globalConfig.cos.region,
28 | };
29 | toPath=toPath.replace(/\\/g,"/");
30 | opt.Key = cosFolder + toPath;
31 | opt.Body = fs.createReadStream(fromPath);
32 | opt.ContentLength = fs.statSync(fromPath).size;
33 | cos.putObject(opt, function (err, data) {
34 | opt.Sign = false;
35 | var url = cos.getObjectUrl(opt);
36 | cb && cb(err, url);
37 | });
38 | }
39 |
40 | module.exports = tool;
41 |
--------------------------------------------------------------------------------
/lib/compress.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var request = require('request');
4 | var pathLib = require('path');
5 | var fs = require('fs');
6 | var tool = require('./common');
7 |
8 | var dns = require('dns');
9 |
10 | var isOA = false;
11 |
12 | dns.resolve('www.oa.com', function(err, address) {
13 | if(!err){
14 | var innerIPReg = /(^10\.|^172\.|^192.)/;
15 | if (innerIPReg.test(address[0])) {
16 | isOA = true;
17 | }
18 | }
19 | });
20 |
21 |
22 | function compress(config, filePath, toPath, cb) {
23 |
24 | var isExists = fs.existsSync('./.tmp');
25 |
26 | if(!isExists) {
27 | fs.mkdirSync('./.tmp');
28 | }
29 |
30 | tool.uploadFile(config, filePath, toPath, function(err, cosFilepath) {
31 | var destPath = '.tmp/'+pathLib.basename(filePath);
32 | var folder = config.folder;
33 |
34 | if(folder && folder.indexOf('/') != 0) {
35 | folder = '/' + folder;
36 | }
37 |
38 | var req;
39 | var picURL = 'http://' + config.bucket + '.pic.' + config.region + '.myqcloud.com' + folder + toPath + '?imageView2/q/70';
40 |
41 | if(!isOA) {
42 | req = request(picURL);
43 | }else {
44 | var _req = request.defaults({'proxy':'http://dev-proxy.oa.com:8080'});
45 | req = _req(picURL);
46 | }
47 |
48 | req.on('response', function() {
49 | var ws = fs.createWriteStream(destPath);
50 | req.pipe(ws);
51 | ws.on('close', function() {
52 | cb(destPath);
53 | });
54 | }).on('error', function(err) {
55 | cb(err);
56 | });
57 | });
58 | }
59 |
60 | module.exports = compress;
61 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var fs = require('fs');
4 | var _ = require('lodash');
5 | var pathLib = require('path');
6 | var CWD = process.cwd();
7 |
8 | /**
9 | * 全局配置
10 | * @type {Object}
11 | * appDir [String] 小程序项目目录,默认./app
12 | * uploadFileSuffix [Array] 上传的图片后缀名,默认jpg, png, gif
13 | * uploadFileBlackList [Path or File] 指定不上传的文件或者路径,不支持正则
14 | * watch [Boolean] 是否实时监听,默认true
15 | * compress [Boolean] 上传的图片是否使用优图的压缩功能,默认false
16 | * cos [Object] 查看https://www.qcloud.com/document/product/436/6066
17 | */
18 |
19 | var _defaultConfig = {
20 | "appDir": "./app",
21 | "backupDir": "./wecos_backup",
22 | "uploadFileSuffix": [".jpg", ".png", ".gif"],
23 | "uploadFileBlackList": [],
24 | "replaceHost": "",
25 | "targetHost": "",
26 | "compress": false,
27 | "watch": true,
28 | "cos": {
29 | "secret_id": "",
30 | "secret_key": "",
31 | "appid": "",
32 | "bucketname": "",
33 | "bucket": "",
34 | "region": "",
35 | "folder": "",
36 | }
37 | };
38 |
39 | function initByOption(option) {
40 | _.assignIn(config, option)
41 | }
42 |
43 | function initByFile(configPath) {
44 | var exists = fs.existsSync(configPath);
45 | if (!exists) {
46 | throw('need file wecos.config.json');
47 | }
48 | var content = fs.readFileSync(configPath).toString();
49 | try {
50 | var userConfig = (new Function('return (' + content + ')'))();
51 | } catch (e) {
52 | throw('wecos.config.json is not JSON format!');
53 | }
54 | _.assignIn(config, userConfig);
55 | var appDir = pathLib.join(CWD, config.appDir);
56 | var appDirExists = fs.existsSync(appDir);
57 | if (!appDirExists) {
58 | throw('appDir is not exist');
59 | }
60 | if (!userConfig.cos) {
61 | throw('option "cos" need in config!');
62 | }
63 | if (userConfig.replaceHost && !userConfig.targetHost) {
64 | throw('option "targetHost" need in config!');
65 | }
66 | var cosOptionNeed = ['region', 'secret_key', 'secret_id'];
67 | for (var i = 0; i < cosOptionNeed.length; i++) {
68 | if (!userConfig.cos[cosOptionNeed[i]]) {
69 | throw('option "cos.' + cosOptionNeed[i] + '" need in config!');
70 | }
71 | }
72 | if (!(userConfig.cos.bucket || (userConfig.cos.bucketname && userConfig.cos.appid))) {
73 | throw('option "cos.bucket" need in config! format as "test-1251902136"');
74 | }
75 | }
76 |
77 | var config = _.assign(_defaultConfig);
78 |
79 | config.init = function(option){
80 | if (!option) {
81 | option = pathLib.join(CWD, 'wecos.config.json');
82 | }
83 | if(_.isString(option)){
84 | initByFile(option)
85 | }else if(_.isObject(option)) {
86 | initByOption(option)
87 | }
88 | if (!config.cos.bucket && config.cos.bucket && config.cos.appid) {
89 | config.cos.bucket = config.cos.bucketname + '-' + config.cos.appid;
90 | }
91 | if (config.cos.bucket && !(config.cos.bucket || config.cos.appid)) {
92 | var lastIndex = config.cos.bucket.lastIndexOf('')
93 | config.cos.bucketname = config.cos.bucket.substr(0, lastIndex)
94 | config.cos.appid = config.cos.bucket.substr(lastIndex + 1)
95 | }
96 | }
97 |
98 | module.exports = config;
99 |
100 |
101 |
--------------------------------------------------------------------------------
/lib/task.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var _ = require('lodash');
3 | var glob = require("glob");
4 | var pathLib = require('path');
5 | var globalConfig = require('./config');
6 | var upload = require('./upload');
7 |
8 | var CWD = process.cwd();
9 | var SUFFIX = ['wxml', 'wxss'];
10 | var REG_PROTOCOL = /^([a-zA-Z]{2,}:)\/\//;
11 | var REG_IMAGE_TAG = /(]*)? src *= *['|"])(.*?)(['|"])/g;
12 | var REG_COVER_IMAGE_TAG = /(]*)? src *= *['|"])(.*?)(['|"])/g;
13 | var REG_IMAGE_STYLE = /(background(-image)? *:[^;]*url *\( *['|"]?)(.*?)(['|"]? *\))/g;
14 | var APP;
15 |
16 |
17 | // 多层创建目录
18 | var deepMkdir = function(dirpath, callback) {
19 | var exists = fs.existsSync(dirpath);
20 | if(!exists) {
21 | deepMkdir(pathLib.dirname(dirpath), callback);
22 | fs.mkdirSync(dirpath, callback);
23 | }
24 | };
25 |
26 | // 备份源文件
27 | var backupFile = function (fromPath, callback) {
28 | if (globalConfig.backupDir === null || globalConfig.backupDir === '') return;
29 | var toPath = pathLib.resolve(globalConfig.backupDir, pathLib.relative(APP, fromPath));
30 | var toDir = pathLib.dirname(toPath);
31 | var cb = function (err) {
32 | callback && callback(err);
33 | };
34 | var backup = function () {
35 | fs.readFile(fromPath, function(err, data) {
36 | if (err) {
37 | cb(err);
38 | } else {
39 | fs.writeFile(toPath, data, function(err){
40 | if (err) {
41 | cb(err);
42 | } else {
43 | if (fs.existsSync(fromPath)) {
44 | fs.unlink(fromPath, cb);
45 | }
46 | }
47 | });
48 | }
49 | });
50 | };
51 | if (fs.existsSync(toDir)) {
52 | backup();
53 | } else {
54 | deepMkdir(toDir, backup);
55 | }
56 | };
57 |
58 | // 格式化 url
59 | var formatUrl = function (url, defaultProtocol) {
60 | defaultProtocol = defaultProtocol || 'http:';
61 | if (url.substr(0, 2) == '//') {
62 | url = defaultProtocol + url;
63 | }
64 | if (!REG_PROTOCOL.test(url)) {
65 | url = defaultProtocol + '//' + url;
66 | }
67 | return url;
68 | };
69 |
70 | // 替换 url 里的 host
71 | var replaceUrlHost = function (url, fromHost, toHost) {
72 | if (!fromHost || !fromHost.length || !toHost || !REG_PROTOCOL.test(url)) return url;
73 | fromHost.forEach(function (host) {
74 | var match = url.match(REG_PROTOCOL);
75 | var protocol = match ? match[1] : 'http:';
76 | var formattedFromHost = formatUrl(host, protocol);
77 | var formattedToHost = formatUrl(toHost, protocol);
78 | if (url.substr(0, formattedFromHost.length) == formattedFromHost) {
79 | var pathname = url.substr(formattedFromHost.length);
80 | var sep = pathname.substr(0, 1) == '/' || formattedToHost.slice(-1) == '/' ? '/' : '';
81 | formattedToHost = formattedToHost.replace(/\/+$/, '');
82 | pathname = pathname.replace(/^\/+/, '');
83 | sep = !sep && pathname ? '/' : sep;
84 | url = formattedToHost + sep + pathname;
85 | }
86 | });
87 | return url;
88 | };
89 |
90 | // 替换文件内的资源文件
91 | var onlineLinks = function (currentPath, resourcePath, url) {
92 | var content = fs.readFileSync(currentPath).toString();
93 | var oldContent = content;
94 | var replaceList = [];
95 | // 先替换 url
96 | var match = url.match(/^([a-zA-Z]{2,}:)\/\/[^/]+/);
97 | if (!match) return replaceList;
98 | if (globalConfig.targetHost) {
99 | url = replaceUrlHost(url, _.concat([], globalConfig.replaceHost, match && match[0]), globalConfig.targetHost);
100 | }
101 | // 找出资源链接,并替换
102 | var replaceHandler = function (s, m1, m2, oldUrl, m4) {
103 | var findResourcePath = oldUrl.substr(0, 1) === '/' ? pathLib.join(APP, oldUrl) :
104 | pathLib.resolve(pathLib.dirname(currentPath), oldUrl);
105 | if (pathLib.resolve(findResourcePath) == pathLib.resolve(resourcePath)) {
106 | replaceList.push({
107 | file: pathLib.relative(APP, currentPath),
108 | from: oldUrl,
109 | to: url
110 | });
111 | return m1 + url + m4;
112 | } else {
113 | return s;
114 | }
115 | };
116 | content = content.replace(REG_IMAGE_TAG, replaceHandler);
117 | content = content.replace(REG_COVER_IMAGE_TAG, replaceHandler);
118 | // 如果文件内容有改变,写入文件
119 | if (oldContent !== content) {
120 | fs.writeFileSync(currentPath, content);
121 | }
122 | return replaceList;
123 | };
124 |
125 | // 替换文件内的链接为新的 Host
126 | var replaceLinks = function (currentPath) {
127 | var replaceList = [];
128 | var fromHost = _.isArray(globalConfig.replaceHost) ? globalConfig.replaceHost : [globalConfig.replaceHost];
129 | var toHost = globalConfig.targetHost;
130 | var content = fs.readFileSync(currentPath).toString();
131 | var oldContent = content;
132 | var _replaceM3 = function (s, m1, m2, oldUrl, m4) {
133 | var url = replaceUrlHost(oldUrl, fromHost, toHost);
134 | if (oldUrl != url) {
135 | replaceList.push({
136 | file: pathLib.relative(APP, currentPath),
137 | from: oldUrl,
138 | to: url
139 | });
140 | }
141 | return m1 + url + m4;
142 | };
143 | content = content.replace(REG_IMAGE_TAG, _replaceM3);
144 | content = content.replace(REG_COVER_IMAGE_TAG, _replaceM3);
145 | content = content.replace(REG_IMAGE_STYLE, _replaceM3);
146 | // 如果文件内容有改变,写入文件
147 | if (oldContent !== content) {
148 | fs.writeFileSync(currentPath, content);
149 | }
150 | return replaceList;
151 | };
152 |
153 | // 上传文件
154 | var uploadFile = (function () {
155 | var count = 0;
156 | var taskQueue = [];
157 | // 最多 5 个并发上传任务
158 | var next = function () {
159 | if (count > 5 || !taskQueue.length) return;
160 | ++count;
161 | var task = taskQueue.shift();
162 | var localPath = pathLib.relative(CWD, task.filepath);
163 | var remotePath = pathLib.relative(APP, task.filepath).replace(pathLib.sep, '/');
164 | upload(localPath, remotePath, function (err, url) {
165 | if (!err) backupFile(task.filepath);
166 | task.callback && task.callback(err, url);
167 | --count;
168 | next();
169 | });
170 | };
171 |
172 | return function (filepath, callback) {
173 | taskQueue.push({
174 | filepath: filepath,
175 | callback: callback
176 | });
177 | next();
178 | };
179 | })();
180 |
181 | // 扫描所有文件,替换成新的链接
182 | var scanFiles = function (iterator, complete) {
183 | glob(pathLib.join(APP, '/**/*') + '.{' + SUFFIX.join(',') + '}', function (er, files) {
184 | var replaceList = [];
185 | files.forEach( function (filepath){
186 | if (SUFFIX.indexOf(filepath.split('.').pop()) > -1) {
187 | var rList = iterator(filepath);
188 | replaceList = _.concat(replaceList, rList);
189 | }
190 | });
191 | complete && complete(null, replaceList);
192 | });
193 | };
194 |
195 | // 上传资源文件并替换线上链接
196 | exports.resourceToCloud = function (resourcePath, callback) {
197 | APP = pathLib.join(CWD, globalConfig.appDir);
198 | fs.lstat(resourcePath, function (err, stat) {
199 | if (stat.size > 0) {
200 | uploadFile(resourcePath, function (err, url) {
201 | if (err) {
202 | callback(err);
203 | } else {
204 | scanFiles(function (currentPath) {
205 | return onlineLinks(currentPath, resourcePath, url);
206 | }, callback);
207 | }
208 | });
209 | } else {
210 | callback && callback('FILE EMPTY');
211 | }
212 | });
213 | };
214 |
215 | // 替换资源文件的域名
216 | exports.resourceReplaceHost = function (callback) {
217 | APP = pathLib.join(CWD, globalConfig.appDir);
218 | if (globalConfig.replaceHost && globalConfig.targetHost) {
219 | scanFiles(replaceLinks, callback);
220 | }
221 | };
222 |
223 | if (!module.parent) {
224 | exports.resourceReplaceHost();
225 | }
--------------------------------------------------------------------------------
/lib/upload.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | //鉴权
4 | //上传
5 | //压缩
6 | var fs = require('fs');
7 | var pathLib = require('path');
8 | var _ = require('lodash');
9 | var globalConfig = require('./config');
10 | var compress = require('./compress');
11 | var tool = require('./common');
12 |
13 | //上传
14 | function upload(fromPath,toPath,cb) {
15 |
16 | var isExists = fs.existsSync(fromPath);
17 |
18 | if(!isExists) {
19 | cb('FILE NOT EXISTS');
20 | return;
21 | }
22 |
23 | if(globalConfig.compress) {
24 | compress(globalConfig.cos, fromPath, toPath, function(destPath) {
25 | if(!_.isString(destPath)) {
26 | var err = destPath;
27 | cb(err);
28 | } else {
29 | tool.uploadFile(globalConfig.cos, destPath, toPath, cb);
30 | }
31 | });
32 | }else {
33 | tool.uploadFile(globalConfig.cos, fromPath, toPath, cb);
34 | }
35 |
36 | }
37 |
38 | module.exports = upload;
39 |
40 |
--------------------------------------------------------------------------------
/lib/watcher.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global require */
4 | var fs = require('fs');
5 | var pathLib = require('path');
6 | var watch = require('watch');
7 | var _ = require('lodash');
8 | var ora = require('ora');
9 | var glob = require('glob');
10 | var globalConfig = require('./config');
11 | var task = require('./task');
12 | var CWD = process.cwd();
13 |
14 |
15 | function normalizePath(path) {
16 | path = pathLib.normalize(path);
17 |
18 | return path;
19 | }
20 |
21 | function joinPath(path) {
22 | var path = pathLib.join(CWD, path);
23 |
24 | return path;
25 | }
26 |
27 | function relativePath(from, to) {
28 | var path = pathLib.relative(from, to);
29 |
30 | return path;
31 | }
32 |
33 | function resolvePath(path) {
34 | var path = pathLib.resolve(path);
35 |
36 | return path;
37 | }
38 |
39 | function fixPathToUnix(path) {
40 |
41 | path = path.replace(/\//g, '\\');
42 |
43 | return path;
44 | }
45 |
46 | function showReplaceList(replaceList) {
47 | replaceList.forEach(function (item) {
48 | console.log('[file:' + item.file + ']', 'replace host from', '"' + item.from + '"', 'to', '"' + item.to + '"');
49 | });
50 | }
51 |
52 | var taskTimer = {};
53 | function runFileTask(filepath, event, stat) {
54 |
55 | // taskTimer[filepath] && clearTimeout(taskTimer[filepath]);
56 | // taskTimer[filepath] = setTimeout(function () {
57 | // taskTimer[filepath] = null;
58 |
59 | if( pathLib.extname(filepath)
60 | && _.includes(globalConfig['uploadFileSuffix'], pathLib.extname(filepath))
61 | ) {
62 |
63 | var isBlackList = _.some(globalConfig.uploadFileBlackList, function(item) {
64 |
65 | if(resolvePath(item) === resolvePath(filepath)) {
66 | return true
67 | }
68 | if(_.startsWith(resolvePath(filepath), resolvePath(item))) {
69 | return true
70 | }
71 | })
72 | if(isBlackList) return;
73 |
74 | if (!fs.existsSync(filepath)) return;
75 |
76 |
77 | filepath = relativePath(CWD, filepath);
78 |
79 | var spinner = ora('upload: ' + filepath).start();
80 | task.resourceToCloud(filepath, function(err) {
81 | if(err) {
82 | spinner.fail();
83 | console.log('Error:', err);
84 | return
85 | }
86 | // console.log(filepath + ' 已传上cos');
87 | spinner.succeed();
88 |
89 | // console.log('event', event, 'filepath', filepath + ' 已传上cos');
90 |
91 | });
92 |
93 | }
94 | // }, 300);
95 |
96 | }
97 |
98 | function watchFile(option) {
99 |
100 | globalConfig.init(option);
101 |
102 | var appDir = relativePath(CWD, resolvePath(globalConfig.appDir));
103 | var uploadSuffix = globalConfig.uploadFileSuffix;
104 | uploadSuffix = _.isArray(uploadSuffix) ? uploadSuffix : [uploadSuffix];
105 | var watchList = _.map(uploadSuffix, function(item) {
106 | return pathLib.join(appDir, '**/*' + item);
107 | });
108 | var uploadBlackList = globalConfig.uploadFileBlackList;
109 | uploadBlackList = _.isArray(uploadBlackList) ? uploadBlackList : [uploadBlackList];
110 | uploadBlackList = _.map(uploadBlackList, function(item) {
111 | if(fs.lstatSync(resolvePath(item)).isFile()) {
112 | return item
113 | }else if(fs.lstatSync(resolvePath(item)).isDirectory()) {
114 | return pathLib.join(item, '**');
115 | }
116 | });
117 |
118 | task.resourceReplaceHost(function (err, replaceList) {
119 | if (!err) {
120 | replaceList.length && console.log('replace host finish:');
121 | showReplaceList(replaceList);
122 | }
123 | });
124 |
125 | console.log('watchList', watchList);
126 | console.log('uploadBlackList', uploadBlackList);
127 |
128 | if (globalConfig.watch) {
129 | console.log('/***** 【微信 COS 瘦身工具】 start watching *****/');
130 | console.log('/***** 【Listening App path】 ' + appDir + ' *****/');
131 |
132 | glob('{' + watchList.join(',') + '}', {
133 | ignore: uploadBlackList
134 | },function (err, files) {
135 | files.forEach(function (filepath) {
136 | runFileTask(filepath);
137 | });
138 | });
139 | watch.createMonitor(appDir, {
140 | interval: 1
141 | },function (monitor) {
142 |
143 | monitor.on("created", function(f, stat) {
144 | runFileTask(f, 'created', stat)
145 | })
146 | monitor.on("changed", function(f, stat) {
147 | runFileTask(f, 'changed', stat)
148 | })
149 |
150 | });
151 | } else {
152 | console.log('/***** 【微信 COS 瘦身工具】 start *****/');
153 | console.log('/***** 【App Path】 ' + appDir + ' *****/');
154 | glob('{' + watchList.join(',') + '}', {
155 | ignore: uploadBlackList
156 | },function (err, files) {
157 | files.forEach(function (filepath) {
158 | runFileTask(filepath);
159 | });
160 | });
161 | }
162 |
163 | }
164 |
165 | module.exports = watchFile;
166 | if (!module.parent) {
167 | watchFile();
168 | console.log('watching!');
169 | }
170 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wecos",
3 | "version": "2.0.3",
4 | "description": "Tencent Qcloud COS about node utilities for wxapp minify.",
5 | "keywords": [
6 | "cos",
7 | "wxapp",
8 | "minify"
9 | ],
10 | "main": "index.js",
11 | "scripts": {
12 | "test": "cd test && node index.js",
13 | "test-config-file": "cd test && node test-config-file.js",
14 | "test-config-option": "cd test && node test-config-option.js"
15 | },
16 | "bin": {
17 | "wecos": "./bin/wecos.js"
18 | },
19 | "homepage": "https://github.com/tencentyun/wecos",
20 | "bugs": "https://github.com/tencentyun/wecos/issues",
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/tencentyun/wecos.git"
24 | },
25 | "author": {
26 | "name": "carsonxu"
27 | },
28 | "maintainers": [
29 | {
30 | "name": "carsonxu"
31 | },
32 | {
33 | "name": "galen-yip",
34 | "email": "s275497985@gmail.com"
35 | },
36 | {
37 | "name": "yinshawnrao"
38 | }
39 | ],
40 | "engines": {
41 | "node": ">=0.9"
42 | },
43 | "license": "MIT",
44 | "dependencies": {
45 | "cos-nodejs-sdk-v5": "^2.4.9",
46 | "glob": "^7.1.1",
47 | "lodash": "^4.17.4",
48 | "ora": "^0.4.0",
49 | "request": "^2.79.0",
50 | "watch": "^1.0.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('../bin/wecos.js');
--------------------------------------------------------------------------------
/test/test-config-file.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var wecos = require('../lib/watcher.js');
4 | wecos('./wecos.config.json');
--------------------------------------------------------------------------------
/test/test-config-option.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var config = require('./wecos.config.js');
3 | var wecos = require('../lib/watcher.js');
4 | wecos(config);
--------------------------------------------------------------------------------
/test/test-task.js:
--------------------------------------------------------------------------------
1 | var resourceToCloud = require('../task');
2 |
3 | var CWD = __dirname;
4 | resourceToCloud([
5 | path.join(CWD, 'app/images/logo.png'),
6 | path.join(CWD, 'app/images/camera.png'),
7 | path.join(CWD, 'app/images/qr.png')
8 | ], function () {
9 | console.log('cloud finished!');
10 | });
--------------------------------------------------------------------------------
/test/test-upload.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var upload = require('../lib/upload');
3 |
4 | upload(path.join(__dirname, 'test.png'), '/test.jpg', function(res) {
5 | console.log(res);
6 | });
7 |
--------------------------------------------------------------------------------
/test/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencentyun/wecos/bc3537e9ececd7bf047255aaee86e04614f1790f/test/test.jpg
--------------------------------------------------------------------------------