├── test └── minicode │ ├── pages │ ├── index │ │ ├── test.wxml │ │ ├── index.wxss │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.js │ └── other │ │ ├── other.wxss │ │ ├── other.json │ │ ├── other.wxml │ │ └── other.js │ ├── subpackages │ ├── buy │ │ ├── buy.wxss │ │ ├── buy.json │ │ ├── buy.wxml │ │ └── buy.js │ ├── order │ │ ├── order.json │ │ ├── order.wxss │ │ ├── order.wxml │ │ └── order.js │ └── .DS_Store │ ├── wxs │ ├── tools.wxs │ └── filter.wxs │ ├── util │ ├── util-c.js │ ├── unused.js │ ├── util.js │ └── util-b.js │ ├── components │ ├── comp.json │ ├── comp.wxml │ └── comp.js │ ├── app.wxss │ ├── .DS_Store │ ├── app.js │ ├── images │ ├── test.gif │ ├── test.jpg │ └── test.png │ ├── sitemap.json │ ├── .jscpd.json │ ├── package.json │ ├── app.json │ ├── package-lock.json │ ├── report │ ├── jscpd-report.json │ └── jscpd-report.html │ └── project.config.json ├── bin └── slim.js ├── .DS_Store ├── .editorconfig ├── config └── jscpd.json ├── docs ├── imagemin.md ├── sprite.md ├── jscpd.md └── deps.md ├── src ├── index.js ├── jscpd.js ├── depsAnalyzer │ ├── utils │ │ ├── setOperation.js │ │ ├── genData.js │ │ ├── util.js │ │ └── unused.js │ ├── handler │ │ ├── component.js │ │ ├── wxss.js │ │ ├── analyzerComp.js │ │ ├── util.js │ │ ├── wxml.js │ │ └── esmodule.js │ └── index.js ├── imagemin.js └── spritesmith.js ├── README.md ├── LICENSE ├── package.json ├── .gitignore └── .eslintrc.js /test/minicode/pages/index/test.wxml: -------------------------------------------------------------------------------- 1 | test.wxml -------------------------------------------------------------------------------- /test/minicode/pages/other/other.wxss: -------------------------------------------------------------------------------- 1 | /* other/other.wxss */ -------------------------------------------------------------------------------- /bin/slim.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../src/index') 3 | -------------------------------------------------------------------------------- /test/minicode/subpackages/buy/buy.wxss: -------------------------------------------------------------------------------- 1 | /* subpackages/buy/buy.wxss */ -------------------------------------------------------------------------------- /test/minicode/wxs/tools.wxs: -------------------------------------------------------------------------------- 1 | module.exports.msg = "some msg"; 2 | -------------------------------------------------------------------------------- /test/minicode/pages/other/other.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /test/minicode/subpackages/buy/buy.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /test/minicode/subpackages/order/order.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /test/minicode/subpackages/order/order.wxss: -------------------------------------------------------------------------------- 1 | /* subpackages/order/order.wxss */ -------------------------------------------------------------------------------- /test/minicode/util/util-c.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | name: 'sgd' 4 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/.DS_Store -------------------------------------------------------------------------------- /test/minicode/components/comp.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /test/minicode/pages/other/other.wxml: -------------------------------------------------------------------------------- 1 | 2 | other/other.wxml 3 | -------------------------------------------------------------------------------- /test/minicode/app.wxss: -------------------------------------------------------------------------------- 1 | @import './miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; 2 | -------------------------------------------------------------------------------- /test/minicode/wxs/filter.wxs: -------------------------------------------------------------------------------- 1 | var tools = require("./tools.wxs"); 2 | var msg = tools.msg 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/minicode/components/comp.wxml: -------------------------------------------------------------------------------- 1 | 2 | components/comp.wxml 3 | -------------------------------------------------------------------------------- /test/minicode/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/test/minicode/.DS_Store -------------------------------------------------------------------------------- /test/minicode/subpackages/buy/buy.wxml: -------------------------------------------------------------------------------- 1 | 2 | subpackages/buy/buy.wxml 3 | -------------------------------------------------------------------------------- /test/minicode/app.js: -------------------------------------------------------------------------------- 1 | const {name} = require('/util/util-c') 2 | 3 | App({ 4 | onLaunch: function () { 5 | 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/minicode/subpackages/order/order.wxml: -------------------------------------------------------------------------------- 1 | 2 | subpackages/order/order.wxml 3 | -------------------------------------------------------------------------------- /test/minicode/images/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/test/minicode/images/test.gif -------------------------------------------------------------------------------- /test/minicode/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/test/minicode/images/test.jpg -------------------------------------------------------------------------------- /test/minicode/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/test/minicode/images/test.png -------------------------------------------------------------------------------- /test/minicode/util/unused.js: -------------------------------------------------------------------------------- 1 | function doSomething() { 2 | console.log('doSomething') 3 | } 4 | 5 | export { 6 | doSomething 7 | } -------------------------------------------------------------------------------- /test/minicode/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .intro { 2 | margin: 30px; 3 | text-align: center; 4 | } 5 | 6 | .demo1 { 7 | height: 60px; 8 | } -------------------------------------------------------------------------------- /test/minicode/subpackages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-slim/HEAD/test/minicode/subpackages/.DS_Store -------------------------------------------------------------------------------- /test/minicode/util/util.js: -------------------------------------------------------------------------------- 1 | import {laugh} from 'util-b' 2 | 3 | function say(name) { 4 | console.log(name) 5 | } 6 | 7 | export { 8 | say 9 | } -------------------------------------------------------------------------------- /test/minicode/util/util-b.js: -------------------------------------------------------------------------------- 1 | import {say} from './util' 2 | 3 | function laugh() { 4 | console.log('laugh') 5 | } 6 | 7 | export { 8 | laugh 9 | } -------------------------------------------------------------------------------- /test/minicode/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /test/minicode/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "mp-loading": "weui-miniprogram/loading/loading", 4 | "mp-searchbar": "weui-miniprogram/searchbar/searchbar", 5 | "mp-wxml-to-canvas": "wxml-to-canvas" 6 | } 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /test/minicode/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/minicode/components/comp.js: -------------------------------------------------------------------------------- 1 | // components/comp.js 2 | Component({ 3 | /** 4 | * 组件的属性列表 5 | */ 6 | properties: { 7 | 8 | }, 9 | 10 | /** 11 | * 组件的初始数据 12 | */ 13 | data: { 14 | 15 | }, 16 | 17 | /** 18 | * 组件的方法列表 19 | */ 20 | methods: { 21 | 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /config/jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporters": ["html", "json", "time"], 3 | "format": ["markup", "javascript", "typescript", "css", "less", "sass", "scss"], 4 | "formatsExts": { 5 | "markup": ["wxml", "html", "xml"], 6 | "javascript": ["js", "wxs"], 7 | "typescript": ["ts"], 8 | "css": ["wxss", "css"], 9 | "less": ["less"], 10 | "sass": ["sass"], 11 | "scss": ["scss"] 12 | } 13 | } -------------------------------------------------------------------------------- /test/minicode/.jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporters": ["html", "json", "time"], 3 | "format": ["markup", "javascript", "typescript", "css", "less", "sass", "scss"], 4 | "formatsExts": { 5 | "markup": ["wxml", "html", "xml"], 6 | "javascript": ["js", "wxs"], 7 | "typescript": ["ts"], 8 | "css": ["wxss", "css"], 9 | "less": ["less"], 10 | "sass": ["sass"], 11 | "scss": ["scss"] 12 | } 13 | } -------------------------------------------------------------------------------- /test/minicode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minicode-75", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "dependencies": { 6 | "miniprogram-sm-crypto": "^0.1.0", 7 | "weui-miniprogram": "^0.2.2", 8 | "wxml-to-canvas": "^1.1.1" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "description": "" 18 | } 19 | -------------------------------------------------------------------------------- /docs/imagemin.md: -------------------------------------------------------------------------------- 1 | # 图片压缩 2 | 3 | 对 `imagemin` 模块的简单封装。 4 | 5 | ## 用法 6 | 7 | ```js 8 | // miniprogram-slim imagemin -h 9 | Usage: miniprogram-slim imagemin [options] 10 | 11 | Minify images seamlessly 12 | 13 | Options: 14 | -o, --output output directory 15 | --png-quality instructs pngquant to use the least amount of colors (default: "0.65,0.8") 16 | --no-progressive creates baseline JPEG file 17 | -h, --help output usage information 18 | ``` -------------------------------------------------------------------------------- /docs/sprite.md: -------------------------------------------------------------------------------- 1 | # 生成雪碧图代码 2 | 3 | 对 `spritesmith` 模块的简单封装,能够自动生成雪碧图和对应的 `css` 代码。 4 | 5 | ## 用法 6 | 7 | ```js 8 | // miniprogram-slim sprite -h 9 | Usage: miniprogram-slim sprite [options] 10 | 11 | Covert images into css sprites 12 | 13 | Options: 14 | -o, --output [dir] output directory (default: "./") 15 | -f, --filename [string] filename of spritesheet (default: "sprite") 16 | -p, --padding [number] padding to use between images (default: 2) 17 | -h, --help output usage information 18 | ``` -------------------------------------------------------------------------------- /test/minicode/pages/index/index.js: -------------------------------------------------------------------------------- 1 | import {say} from '../../util/util' 2 | say('indexindex') 3 | 4 | const app = getApp() 5 | 6 | Page({ 7 | data: { 8 | 9 | }, 10 | onLoad: function () { 11 | console.log('代码片段是一种迷你、可分享的小程序或小游戏项目,可用于分享小程序和小游戏的开发经验、展示组件和 API 的使用、复现开发问题和 Bug 等。可点击以下链接查看代码片段的详细文档:') 12 | console.log('https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html') 13 | }, 14 | 15 | chooseImage() { 16 | wx.chooseImage({ 17 | complete: (res) => { 18 | console.log('res', res) 19 | }, 20 | }) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /docs/jscpd.md: -------------------------------------------------------------------------------- 1 | # 代码相似度比较 2 | 3 | 对 `jscpd` 模块的简单封装,默认会在执行的目录下生成一份 `.jscpd.json` 配置文件,`report` 目录保存生成的代码对比报告。 4 | 5 | ## 用法 6 | 7 | ```js 8 | // miniprogram-slim cpd -h 9 | sage: miniprogram-slim cpd [options] 10 | 11 | Detect duplications in source code 12 | 13 | Options: 14 | -c, --config [file] path to config file (default: ".jscpd.json") 15 | -o, --output [dir] path to directory for reports (default: "./report/") 16 | -i, --ignore glob pattern for files what should be excluded from duplication detection 17 | -h, --help output usage information 18 | ``` -------------------------------------------------------------------------------- /test/minicode/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/other/other" 5 | ], 6 | "window": { 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle": "black" 11 | }, 12 | "style": "v2", 13 | "sitemapLocation": "sitemap.json", 14 | "usingComponents": { 15 | "mp-searchbar": "weui-miniprogram/searchbar/searchbar" 16 | }, 17 | "subpackages": [ 18 | { 19 | "root": "subpackages/buy/", 20 | "pages": [ 21 | "buy" 22 | ] 23 | }, { 24 | "root": "subpackages/order/", 25 | "pages": [ 26 | "order" 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | require('./jscpd') 3 | require('./spritesmith') 4 | require('./imagemin') 5 | require('./depsAnalyzer/index') 6 | 7 | const packageJson = require('../package.json') 8 | 9 | program 10 | .version(packageJson.version, '-v, --version', 'output the version number') 11 | .name('miniprogram-slim') 12 | .usage('') 13 | 14 | program.on('--help', () => { 15 | console.log('') 16 | console.log('Examples:') 17 | console.log(' $ miniprogram-slim analyzer -t') 18 | console.log(' $ miniprogram-slim cpd src') 19 | console.log(' $ miniprogram-slim imagemin images/**/*.png') 20 | console.log(' $ miniprogram-slim sprite -f emoji images/**/*.png') 21 | }) 22 | 23 | program.parse(process.argv) 24 | -------------------------------------------------------------------------------- /test/minicode/subpackages/buy/buy.js: -------------------------------------------------------------------------------- 1 | // subpackages/buy/buy.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | /** 12 | * 生命周期函数--监听页面加载 13 | */ 14 | onLoad: function (options) { 15 | 16 | }, 17 | 18 | /** 19 | * 生命周期函数--监听页面初次渲染完成 20 | */ 21 | onReady: function () { 22 | 23 | }, 24 | 25 | /** 26 | * 生命周期函数--监听页面显示 27 | */ 28 | onShow: function () { 29 | 30 | }, 31 | 32 | /** 33 | * 生命周期函数--监听页面隐藏 34 | */ 35 | onHide: function () { 36 | 37 | }, 38 | 39 | /** 40 | * 生命周期函数--监听页面卸载 41 | */ 42 | onUnload: function () { 43 | 44 | }, 45 | 46 | /** 47 | * 页面相关事件处理函数--监听用户下拉动作 48 | */ 49 | onPullDownRefresh: function () { 50 | 51 | }, 52 | 53 | /** 54 | * 页面上拉触底事件的处理函数 55 | */ 56 | onReachBottom: function () { 57 | 58 | }, 59 | 60 | /** 61 | * 用户点击右上角分享 62 | */ 63 | onShareAppMessage: function () { 64 | 65 | } 66 | }) -------------------------------------------------------------------------------- /test/minicode/subpackages/order/order.js: -------------------------------------------------------------------------------- 1 | // subpackages/order/order.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | /** 12 | * 生命周期函数--监听页面加载 13 | */ 14 | onLoad: function (options) { 15 | 16 | }, 17 | 18 | /** 19 | * 生命周期函数--监听页面初次渲染完成 20 | */ 21 | onReady: function () { 22 | 23 | }, 24 | 25 | /** 26 | * 生命周期函数--监听页面显示 27 | */ 28 | onShow: function () { 29 | 30 | }, 31 | 32 | /** 33 | * 生命周期函数--监听页面隐藏 34 | */ 35 | onHide: function () { 36 | 37 | }, 38 | 39 | /** 40 | * 生命周期函数--监听页面卸载 41 | */ 42 | onUnload: function () { 43 | 44 | }, 45 | 46 | /** 47 | * 页面相关事件处理函数--监听用户下拉动作 48 | */ 49 | onPullDownRefresh: function () { 50 | 51 | }, 52 | 53 | /** 54 | * 页面上拉触底事件的处理函数 55 | */ 56 | onReachBottom: function () { 57 | 58 | }, 59 | 60 | /** 61 | * 用户点击右上角分享 62 | */ 63 | onShareAppMessage: function () { 64 | 65 | } 66 | }) -------------------------------------------------------------------------------- /test/minicode/pages/other/other.js: -------------------------------------------------------------------------------- 1 | const sm2 = require('miniprogram-sm-crypto').sm2 2 | 3 | // other/other.js 4 | Page({ 5 | 6 | /** 7 | * 页面的初始数据 8 | */ 9 | data: { 10 | 11 | }, 12 | 13 | /** 14 | * 生命周期函数--监听页面加载 15 | */ 16 | onLoad: function (options) { 17 | 18 | }, 19 | 20 | /** 21 | * 生命周期函数--监听页面初次渲染完成 22 | */ 23 | onReady: function () { 24 | 25 | }, 26 | 27 | /** 28 | * 生命周期函数--监听页面显示 29 | */ 30 | onShow: function () { 31 | 32 | }, 33 | 34 | /** 35 | * 生命周期函数--监听页面隐藏 36 | */ 37 | onHide: function () { 38 | 39 | }, 40 | 41 | /** 42 | * 生命周期函数--监听页面卸载 43 | */ 44 | onUnload: function () { 45 | 46 | }, 47 | 48 | /** 49 | * 页面相关事件处理函数--监听用户下拉动作 50 | */ 51 | onPullDownRefresh: function () { 52 | 53 | }, 54 | 55 | /** 56 | * 页面上拉触底事件的处理函数 57 | */ 58 | onReachBottom: function () { 59 | 60 | }, 61 | 62 | /** 63 | * 用户点击右上角分享 64 | */ 65 | onShareAppMessage: function () { 66 | 67 | } 68 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小程序瘦身工具 2 | 3 | 通过剔除无用文件、压缩图片、复用代码等方式减少小程序代码包体积。 4 | 5 | ## 功能 6 | 7 | * [依赖分析,查找无用文件](./docs/deps.md) 8 | * [代码相似度比较](./docs/jscpd.md) 9 | * [生成雪碧图代码](./docs/sprite.md) 10 | * [图片压缩](./docs/imagemin.md) 11 | 12 | ## 安装 13 | 14 | ```js 15 | npm install -g miniprogram-slim 16 | ``` 17 | 18 | ## 使用 19 | 20 | ```js 21 | Usage: miniprogram-slim 22 | 23 | Options: 24 | -v, --version output the version number 25 | -h, --help output usage information 26 | 27 | Commands: 28 | cpd [options] Detect duplications in source code 29 | sprite [options] Covert images into css sprites 30 | imagemin [options] Minify images seamlessly 31 | analyzer [options] Analyze dependencies of miniprogram, find out unused files 32 | 33 | Examples: 34 | $ miniprogram-slim analyzer -t 35 | $ miniprogram-slim cpd src 36 | $ miniprogram-slim imagemin images/**/*.png 37 | $ miniprogram-slim sprite -f emoji images/**/*.png 38 | ``` 39 | -------------------------------------------------------------------------------- /src/jscpd.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const shell = require('shelljs') 3 | const fs = require('fs-extra') 4 | const {resolve} = require('path') 5 | 6 | const path2JscpdJson = resolve(__dirname, '../config/jscpd.json') 7 | const path2JscpdBin = resolve(__dirname, '../node_modules/jscpd/bin/jscpd') 8 | const jscpdAction = (dir, cli) => { 9 | const config = cli.config ? resolve(cli.config) : '.jscpd.json' 10 | if (!fs.existsSync(config)) { 11 | fs.copySync(path2JscpdJson, config) 12 | } 13 | const ignore = cli.ignore ? `-i "${cli.ignore}" ` : '' 14 | const command = `${path2JscpdBin} -c ${config} -o ${cli.output} ${ignore} ${cli.blame ? '-b ' : ''} ${dir} ` 15 | shell.exec(command) 16 | } 17 | 18 | program 19 | .command('cpd ') 20 | .description('Detect duplications in source code') 21 | .option('-c, --config [file]', 'path to config file', '.jscpd.json') 22 | .option('-o, --output [dir]', 'path to directory for reports', './report/') 23 | .option('-i, --ignore ', 'glob pattern for files what should be excluded from duplication detection') 24 | .action(jscpdAction) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wechat-miniprogram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/depsAnalyzer/utils/setOperation.js: -------------------------------------------------------------------------------- 1 | function isSuperset(set, subset) { 2 | for (const elem of subset) { 3 | if (!set.has(elem)) { 4 | return false 5 | } 6 | } 7 | return true 8 | } 9 | 10 | function union(setA, setB) { 11 | const _union = new Set(setA) 12 | for (const elem of setB) { 13 | _union.add(elem) 14 | } 15 | return _union 16 | } 17 | 18 | function intersection(setA, setB) { 19 | const _intersection = new Set() 20 | for (const elem of setB) { 21 | if (setA.has(elem)) { 22 | _intersection.add(elem) 23 | } 24 | } 25 | return _intersection 26 | } 27 | 28 | function symmetricDifference(setA, setB) { 29 | const _difference = new Set(setA) 30 | for (const elem of setB) { 31 | if (_difference.has(elem)) { 32 | _difference.delete(elem) 33 | } else { 34 | _difference.add(elem) 35 | } 36 | } 37 | return _difference 38 | } 39 | 40 | function difference(setA, setB) { 41 | const _difference = new Set(setA) 42 | for (const elem of setB) { 43 | _difference.delete(elem) 44 | } 45 | return _difference 46 | } 47 | 48 | module.exports = { 49 | isSuperset, 50 | union, 51 | intersection, 52 | symmetricDifference, 53 | difference 54 | } 55 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/component.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const {suffixExtname} = require('../utils/util') 3 | const {findAbsolutePath} = require('./util') 4 | 5 | // 忽略插件中的组件 6 | const singleJsonAnalyzer = (filePath) => { 7 | const config = fs.readJSONSync(filePath) 8 | const usingComponents = config.usingComponents || {} 9 | const deps = {} 10 | Object.keys(usingComponents).forEach(comp => { 11 | const relativePath = usingComponents[comp] 12 | if (!relativePath.startsWith('plugin://')) { 13 | const depCompPath = findAbsolutePath({ 14 | filePath, 15 | relativePath, 16 | ext: 'json' 17 | }) 18 | if (depCompPath) { 19 | deps[comp] = depCompPath 20 | } 21 | } 22 | }) 23 | return { 24 | filePath, 25 | deps 26 | } 27 | } 28 | 29 | const genCompDepsGraph = (entry) => { 30 | entry = suffixExtname(entry, 'json') 31 | if (!fs.existsSync(entry)) return {} 32 | 33 | const entryModule = singleJsonAnalyzer(entry) 34 | const deps = entryModule.deps 35 | const depsGraph = {[entry]: deps} 36 | 37 | Object.values(deps).forEach(entry => { 38 | Object.assign(depsGraph, genCompDepsGraph(entry)) 39 | }) 40 | return depsGraph 41 | } 42 | 43 | const genCompDepsMap = (compDepsGraph) => { 44 | const compMap = {} 45 | Object.values(compDepsGraph).forEach(item => { 46 | Object.assign(compMap, item) 47 | }) 48 | const compDeps = Object.values(compMap) 49 | return compDeps 50 | } 51 | 52 | 53 | module.exports = { 54 | genCompDepsGraph, 55 | genCompDepsMap, 56 | } 57 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/wxss.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const css = require('css') 3 | const {suffixExtname} = require('../utils/util') 4 | const {findAbsolutePath} = require('./util') 5 | 6 | const traverseWxss = (ast) => { 7 | const result = [] // 找出 import 引入的文件 8 | const rules = ast.stylesheet.rules 9 | rules.forEach(rule => { 10 | if (rule.type === 'import') { 11 | const route = rule.import.replace(/'|"/g, '') 12 | result.push(route) 13 | } 14 | }) 15 | return result 16 | } 17 | 18 | // 找出 import 引入的文件 19 | const singleWxssAnalyser = (filePath) => { 20 | const wxss = fs.readFileSync(filePath, 'utf-8') 21 | const ast = css.parse(wxss, {source: filePath}) 22 | 23 | const deps = {} 24 | const depWxssFiles = traverseWxss(ast) 25 | depWxssFiles.forEach(relativePath => { 26 | const depFilePath = findAbsolutePath({ 27 | filePath, 28 | relativePath, 29 | ext: 'wxss' 30 | }) 31 | if (depFilePath) { 32 | deps[relativePath] = depFilePath 33 | } 34 | }) 35 | 36 | return { 37 | filePath, 38 | deps, 39 | } 40 | } 41 | 42 | const genWxssDepsGraph = (entry) => { 43 | entry = suffixExtname(entry, 'wxss') 44 | if (!fs.existsSync(entry)) return {} 45 | 46 | const entryModule = singleWxssAnalyser(entry) 47 | const deps = entryModule.deps 48 | const depsGraph = {[entry]: deps} 49 | 50 | Object.values(deps).forEach(entry => { 51 | Object.assign(depsGraph, genWxssDepsGraph(entry)) 52 | }) 53 | return depsGraph 54 | } 55 | 56 | module.exports = { 57 | genWxssDepsGraph 58 | } 59 | -------------------------------------------------------------------------------- /src/imagemin.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const ora = require('ora') 3 | const imagemin = require('imagemin') 4 | const imageminJpegtran = require('imagemin-jpegtran') 5 | const imageminPngquant = require('imagemin-pngquant') 6 | const imageminGifsicle = require('imagemin-gifsicle') 7 | 8 | async function imageminAction(input, cli) { 9 | const {progressive, output, pngQuality} = cli 10 | let quality = [] 11 | if (pngQuality) { 12 | quality = pngQuality.split(',').map(item => parseFloat(item)) 13 | } 14 | const plugins = [ 15 | imageminJpegtran({ 16 | progressive 17 | }), 18 | imageminPngquant({ 19 | verbose: true, 20 | quality: quality.length === 2 ? quality : [0.6, 0.8] 21 | }), 22 | imageminGifsicle({ 23 | interlaced: progressive 24 | }) 25 | ] 26 | const spinner = ora('Minifying images') 27 | if (output) { 28 | spinner.start() 29 | } 30 | 31 | let files 32 | try { 33 | files = await imagemin(input, {destination: output, plugins}) 34 | } catch (error) { 35 | spinner.stop() 36 | throw error 37 | } 38 | 39 | if (!output && files.length === 0) { 40 | return 41 | } 42 | 43 | if (!output && files.length > 1) { 44 | console.error('Cannot write multiple files to stdout, specify `-o or --output`') 45 | process.exit(1) 46 | } 47 | 48 | if (!output) { 49 | process.stdout.write(files[0].data) 50 | return 51 | } 52 | 53 | spinner.stop() 54 | console.log(`${files.length} images minified`) 55 | } 56 | 57 | program 58 | .command('imagemin ') 59 | .description('Minify images seamlessly') 60 | .option('-o, --output ', 'output directory') 61 | .option('--png-quality ', 'instructs pngquant to use the least amount of colors', '0.65,0.8') 62 | .option('--no-progressive', 'creates baseline JPEG file') 63 | .action(imageminAction) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-slim", 3 | "version": "1.0.0-beta.1", 4 | "description": "analyze miniprogram dependencies, find unused files", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "husky": { 9 | "hooks": { 10 | "pre-commit": "lint-staged", 11 | "pre-push": "npm test" 12 | } 13 | }, 14 | "lint-staged": { 15 | "src/**/*.js": [ 16 | "prettier --write", 17 | "eslint --fix", 18 | "git add" 19 | ] 20 | }, 21 | "keywords": [ 22 | "miniprogram", 23 | "slim", 24 | "dependencies analyzer", 25 | "useless file" 26 | ], 27 | "author": "sanfordsun", 28 | "license": "MIT", 29 | "bin": { 30 | "miniprogram-slim": "./bin/slim.js" 31 | }, 32 | "dependencies": { 33 | "@babel/parser": "^7.8.7", 34 | "@babel/traverse": "^7.8.6", 35 | "babel-core": "^6.26.3", 36 | "babel-types": "^6.26.0", 37 | "clean-css": "^4.2.3", 38 | "cli-table3": "^0.6.0", 39 | "colors": "^1.4.0", 40 | "commander": "^4.1.1", 41 | "css": "^2.2.4", 42 | "execution-time": "^1.4.1", 43 | "fs-extra": "^8.1.0", 44 | "glob": "^7.1.6", 45 | "imagemin": "^7.0.1", 46 | "imagemin-gifsicle": "^7.0.0", 47 | "imagemin-jpegtran": "^6.0.0", 48 | "imagemin-pngquant": "^8.0.0", 49 | "inquirer": "^7.0.6", 50 | "jscpd": "^3.0.0", 51 | "node-html-parser": "^1.2.14", 52 | "ora": "^4.0.3", 53 | "prettier": "^2.0.4", 54 | "shelljs": "^0.8.3", 55 | "simple-node-logger": "^18.12.24", 56 | "spritesmith": "^3.4.0" 57 | }, 58 | "devDependencies": { 59 | "eslint": "^6.8.0", 60 | "eslint-config-airbnb-base": "^14.1.0", 61 | "eslint-plugin-import": "^2.20.2", 62 | "eslint-plugin-node": "^11.1.0", 63 | "eslint-plugin-promise": "^4.2.1", 64 | "husky": "^4.2.5", 65 | "lint-staged": "^10.1.6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/analyzerComp.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {genWxmlDepsGraph, genWxsDepsMap} = require('./wxml') 3 | const {genEsModuleDepsGraph, genWxsModuleDepsGraph} = require('./esmodule') 4 | const {genCompDepsGraph, genCompDepsMap} = require('./component') 5 | const {genWxssDepsGraph} = require('./wxss') 6 | const {suffixExtname} = require('../utils/util') 7 | 8 | // 分析组件的依赖情况,页面也可视为一个组件 9 | const analyzeComponent = (entry) => { 10 | const esmoduleDepsGraph = genEsModuleDepsGraph(entry) 11 | const wxmlDepsGraph = genWxmlDepsGraph(entry) 12 | const wxssDepsGraph = genWxssDepsGraph(entry) 13 | const compDepsGraph = genCompDepsGraph(entry) 14 | const wxsMap = genWxsDepsMap(wxmlDepsGraph) 15 | const wxsDeps = [] 16 | Object.values(wxsMap).forEach(wxsEntry => { 17 | const wxsDepsGraph = genWxsModuleDepsGraph(wxsEntry) 18 | const perWxsDeps = Object.keys(wxsDepsGraph.map) 19 | wxsDeps.push(...perWxsDeps) 20 | }) 21 | let jsonPath = suffixExtname(entry, 'json') 22 | jsonPath = fs.existsSync(jsonPath) ? jsonPath : '' 23 | 24 | const esDeps = Object.keys(esmoduleDepsGraph.map) 25 | const wxmlDeps = Object.keys(wxmlDepsGraph) 26 | const wxssDeps = Object.keys(wxssDepsGraph) 27 | const compDeps = genCompDepsMap(compDepsGraph) 28 | const jsonDeps = [jsonPath] 29 | const files = [...wxmlDeps, ...wxssDeps, ...wxsDeps, ...esDeps, ...jsonDeps] 30 | 31 | return { 32 | esDeps, 33 | wxmlDeps, 34 | wxssDeps, 35 | compDeps, 36 | wxsDeps, 37 | jsonDeps, 38 | files 39 | } 40 | } 41 | 42 | const computeComponentSize = (compDep, allFileInfo) => { 43 | let totalSize = 0 44 | compDep.files.forEach(file => { 45 | const size = allFileInfo[file].size 46 | totalSize += size 47 | }) 48 | 49 | return +totalSize.toFixed(2) 50 | } 51 | 52 | 53 | module.exports = { 54 | analyzeComponent, 55 | computeComponentSize 56 | } 57 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const {suffixExtname} = require('../utils/util') 4 | 5 | const findNpmPath = ({ 6 | cwd, 7 | relativePath, 8 | ext 9 | }) => { 10 | const dir = 'miniprogram_npm/' 11 | const absoluteRoot = process.cwd() 12 | const weappNpmPath = path.join(cwd, dir) 13 | let absolutePath 14 | 15 | if (fs.existsSync(weappNpmPath)) { 16 | // 可能是npm 包文件 17 | absolutePath = path.join(weappNpmPath, relativePath) 18 | absolutePath = suffixExtname(absolutePath, ext) 19 | if (fs.existsSync(absolutePath)) { 20 | return path.relative(absoluteRoot, absolutePath) 21 | } 22 | 23 | // 可能是npm 包目录 24 | absolutePath = path.join(weappNpmPath, relativePath, `index.${ext}`) 25 | if (fs.existsSync(absolutePath)) { 26 | return path.relative(absoluteRoot, absolutePath) 27 | } 28 | } 29 | 30 | if (cwd === absoluteRoot) { 31 | return null 32 | } 33 | 34 | return findNpmPath({ 35 | cwd: path.resolve(cwd, '../'), 36 | relativePath, 37 | ext 38 | }) 39 | } 40 | 41 | const findAbsolutePath = ({ 42 | filePath, 43 | relativePath, 44 | ext 45 | }) => { 46 | // 包内文件: 根目录、相对目录 47 | const dirname = path.dirname(filePath) 48 | let absolutePath = null 49 | // 当前在根目录之下 50 | if (relativePath.startsWith('/')) { 51 | absolutePath = path.join('./', relativePath) 52 | } else { 53 | absolutePath = path.join(dirname, relativePath) 54 | } 55 | absolutePath = suffixExtname(absolutePath, ext) 56 | 57 | if (fs.existsSync(absolutePath)) { 58 | return absolutePath 59 | } 60 | if (ext === 'js' || ext === 'json') { 61 | absolutePath = findNpmPath({ 62 | relativePath, 63 | cwd: path.resolve(dirname, './'), 64 | ext 65 | }) 66 | if (absolutePath) { 67 | return absolutePath 68 | } 69 | } 70 | return null 71 | } 72 | 73 | module.exports = { 74 | findAbsolutePath 75 | } 76 | -------------------------------------------------------------------------------- /src/spritesmith.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const fs = require('fs-extra') 3 | const path = require('path') 4 | const Spritesmith = require('spritesmith') 5 | const CleanCSS = require('clean-css') 6 | 7 | const parseInt = (value) => parseInt(value) 8 | 9 | const spriteAction = (input, cli) => { 10 | const {filename, padding, output} = cli 11 | Spritesmith.run({ 12 | src: input, 13 | padding 14 | }, (err, result) => { 15 | if (err) { 16 | console.error(`Failed to construct sprite map: ${err}`) 17 | return 18 | } 19 | 20 | const {coordinates, image: buffer} = result 21 | const styles = [] 22 | const classNames = [] 23 | // eslint-disable-next-line guard-for-in 24 | for (const key in coordinates) { 25 | const box = coordinates[key] 26 | const basename = path.basename(key).split('.')[0] 27 | const className = `.${basename}` 28 | classNames.push(className) 29 | 30 | styles.push(` 31 | ${className} { 32 | width: ${box.width}px; 33 | height: ${box.height}px; 34 | background-position: ${-box.x}px ${-box.y}px; 35 | }`) 36 | } 37 | 38 | styles.unshift(` 39 | ${classNames.join(',')} { 40 | display: inline-block; 41 | background-repeat: no-repeat; 42 | background-image: url("./${filename}.png"); 43 | } 44 | `) 45 | 46 | styles.unshift('/*!-----The css code below is created by----*/') 47 | 48 | const spritesheet = styles.join('\n') 49 | const prettySpritesheet = new CleanCSS({ 50 | format: 'beautify' 51 | }).minify(spritesheet).styles 52 | 53 | const directory = path.resolve(output) 54 | fs.ensureDirSync(directory) 55 | fs.writeFileSync(path.resolve(directory, `${filename}.css`), prettySpritesheet) 56 | fs.writeFileSync(path.resolve(directory, `${filename}.png`), buffer) 57 | }) 58 | } 59 | 60 | program 61 | .command('sprite ') 62 | .description('Covert images into css sprites') 63 | .option('-o, --output [dir]', 'output directory', './') 64 | .option('-f, --filename [string]', 'filename of spritesheet', 'sprite') 65 | .option('-p, --padding [number]', 'padding to use between images', parseInt, 2) 66 | .action(spriteAction) 67 | -------------------------------------------------------------------------------- /test/minicode/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minicode-75", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "eventemitter3": { 8 | "version": "4.0.0", 9 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", 10 | "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" 11 | }, 12 | "jsbn": { 13 | "version": "1.1.0", 14 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", 15 | "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" 16 | }, 17 | "lodash": { 18 | "version": "4.17.15", 19 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 20 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 21 | }, 22 | "miniprogram-sm-crypto": { 23 | "version": "0.1.0", 24 | "resolved": "https://registry.npmjs.org/miniprogram-sm-crypto/-/miniprogram-sm-crypto-0.1.0.tgz", 25 | "integrity": "sha512-LVqqteGNCarEepLezmKVMxuD1V795EIN1+Js4SU2nWEqnH50mwv93Vivp3F6LIWFMLE9Il4Cow1QIoqgoOTzyA==", 26 | "requires": { 27 | "jsbn": "^1.1.0" 28 | } 29 | }, 30 | "weui-miniprogram": { 31 | "version": "0.2.2", 32 | "resolved": "https://registry.npmjs.org/weui-miniprogram/-/weui-miniprogram-0.2.2.tgz", 33 | "integrity": "sha512-Gn1ahB3YyfnMO+KPRl4ixGQZnbefmS4NZPBiY50FjLO6aBqt3xUvnXaT1B2AhMWfsUq2dWLvedJqCTmi0hP/jg==" 34 | }, 35 | "widget-ui": { 36 | "version": "1.0.2", 37 | "resolved": "https://registry.npmjs.org/widget-ui/-/widget-ui-1.0.2.tgz", 38 | "integrity": "sha512-gDXosr5mflJdMA1weU1A47aTsTFfMJhfA4EKgO5XFebY3eVklf80KD4GODfrjo8J2WQ+9YjL1Rd9UUmKIzhShw==", 39 | "requires": { 40 | "eventemitter3": "^4.0.0" 41 | } 42 | }, 43 | "wxml-to-canvas": { 44 | "version": "1.1.1", 45 | "resolved": "https://registry.npmjs.org/wxml-to-canvas/-/wxml-to-canvas-1.1.1.tgz", 46 | "integrity": "sha512-3mDjHzujY/UgdCOXij/MnmwJYerVjwkyQHMBFBE8zh89DK7h7UTzoydWFqEBjIC0rfZM+AXl5kDh9hUcsNpSmg==", 47 | "requires": { 48 | "widget-ui": "^1.0.2" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/minicode/report/jscpd-report.json: -------------------------------------------------------------------------------- 1 | { 2 | "duplicates": [], 3 | "statistics": { 4 | "detectionDate": "2020-04-20T11:05:35.683Z", 5 | "formats": { 6 | "javascript": { 7 | "sources": { 8 | "/Users/sanfordsun/tencent/cli/test/minicode/pages/index/index.js": { 9 | "lines": 22, 10 | "sources": 1, 11 | "clones": 0, 12 | "duplicatedLines": 0, 13 | "percentage": 0, 14 | "newDuplicatedLines": 0, 15 | "newClones": 0 16 | }, 17 | "/Users/sanfordsun/tencent/cli/test/minicode/pages/other/other.js": { 18 | "lines": 68, 19 | "sources": 1, 20 | "clones": 0, 21 | "duplicatedLines": 0, 22 | "percentage": 0, 23 | "newDuplicatedLines": 0, 24 | "newClones": 0 25 | } 26 | }, 27 | "total": { 28 | "lines": 90, 29 | "sources": 2, 30 | "clones": 0, 31 | "duplicatedLines": 0, 32 | "percentage": 0, 33 | "newDuplicatedLines": 0, 34 | "newClones": 0 35 | } 36 | }, 37 | "markup": { 38 | "sources": { 39 | "/Users/sanfordsun/tencent/cli/test/minicode/pages/index/index.wxml": { 40 | "lines": 6, 41 | "sources": 1, 42 | "clones": 0, 43 | "duplicatedLines": 0, 44 | "percentage": 0, 45 | "newDuplicatedLines": 0, 46 | "newClones": 0 47 | } 48 | }, 49 | "total": { 50 | "lines": 6, 51 | "sources": 1, 52 | "clones": 0, 53 | "duplicatedLines": 0, 54 | "percentage": 0, 55 | "newDuplicatedLines": 0, 56 | "newClones": 0 57 | } 58 | }, 59 | "css": { 60 | "sources": { 61 | "/Users/sanfordsun/tencent/cli/test/minicode/pages/index/index.wxss": { 62 | "lines": 8, 63 | "sources": 1, 64 | "clones": 0, 65 | "duplicatedLines": 0, 66 | "percentage": 0, 67 | "newDuplicatedLines": 0, 68 | "newClones": 0 69 | } 70 | }, 71 | "total": { 72 | "lines": 8, 73 | "sources": 1, 74 | "clones": 0, 75 | "duplicatedLines": 0, 76 | "percentage": 0, 77 | "newDuplicatedLines": 0, 78 | "newClones": 0 79 | } 80 | } 81 | }, 82 | "total": { 83 | "lines": 104, 84 | "sources": 4, 85 | "clones": 0, 86 | "duplicatedLines": 0, 87 | "percentage": 0, 88 | "newDuplicatedLines": 0, 89 | "newClones": 0 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/minicode/node_modules 2 | test/minicode/miniprogram_npm 3 | test/minicode/analyzer 4 | .DS_Store 5 | test/.DS_Store 6 | package-lock.json 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/wxml.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const HTMLParser = require('node-html-parser') 3 | const {suffixExtname} = require('../utils/util') 4 | const {findAbsolutePath} = require('./util') 5 | 6 | const traverseWxml = (root) => { 7 | const result = [] 8 | const tagName = root.tagName 9 | if (root.nodeType !== 1) return result 10 | 11 | const keywords = ['import', 'include', 'wxs'] 12 | if (keywords.includes(tagName)) { 13 | const rawAttrs = root.rawAttrs.split(' ') 14 | rawAttrs.forEach(attr => { 15 | const pairs = attr.split('=') 16 | if (pairs[0] === 'src' && pairs[1]) { 17 | const route = pairs[1].replace(/'|"/g, '') 18 | const type = tagName === 'wxs' ? 'wxs' : 'wxml' 19 | result.push({ 20 | type, 21 | route 22 | }) 23 | } 24 | }) 25 | } 26 | const childNodes = root.childNodes 27 | childNodes.forEach(child => { 28 | const childResult = traverseWxml(child) 29 | result.push(...childResult) 30 | }) 31 | return result 32 | } 33 | 34 | // 找出 import & include 引用的路径依赖 35 | const singleWxmlAnalyser = (filePath) => { 36 | const wxml = fs.readFileSync(filePath, 'utf-8') 37 | const root = HTMLParser.parse(wxml, { 38 | lowerCaseTagName: true, 39 | script: false, 40 | style: false, 41 | pre: false, 42 | comment: false 43 | }) 44 | 45 | const wxmlDeps = {} 46 | const wxsDeps = {} 47 | const depFiles = traverseWxml(root) 48 | depFiles.forEach(item => { 49 | const {type, route} = item 50 | const depFilePath = findAbsolutePath({ 51 | filePath, 52 | relativePath: route, 53 | ext: type 54 | }) 55 | if (depFilePath) { 56 | if (type === 'wxml') { 57 | wxmlDeps[route] = depFilePath 58 | } else { 59 | wxsDeps[route] = depFilePath 60 | } 61 | } 62 | }) 63 | 64 | return { 65 | filePath, 66 | wxmlDeps, 67 | wxsDeps 68 | } 69 | } 70 | 71 | const genWxmlDepsGraph = (entry) => { 72 | entry = suffixExtname(entry, 'wxml') 73 | if (!fs.existsSync(entry)) return {} 74 | 75 | const entryModule = singleWxmlAnalyser(entry) 76 | const {wxsDeps, wxmlDeps} = entryModule 77 | const depsGraph = { 78 | [entry]: { 79 | wxsDeps, 80 | wxmlDeps 81 | } 82 | } 83 | 84 | Object.values(wxmlDeps).forEach(entry => { 85 | Object.assign(depsGraph, genWxmlDepsGraph(entry)) 86 | }) 87 | return depsGraph 88 | } 89 | 90 | const genWxsDepsMap = (wxmlDepsGraph) => { 91 | const wxsMap = {} 92 | Object.values(wxmlDepsGraph).forEach(item => { 93 | Object.assign(wxsMap, item.wxsDeps) 94 | }) 95 | return wxsMap 96 | } 97 | 98 | module.exports = { 99 | genWxmlDepsGraph, 100 | genWxsDepsMap 101 | } 102 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base' 4 | ], 5 | 'parserOptions': { 6 | 'ecmaVersion': 9, 7 | 'ecmaFeatures': { 8 | 'jsx': false 9 | }, 10 | 'sourceType': 'module' 11 | }, 12 | 'env': { 13 | 'es6': true, 14 | 'node': true, 15 | 'jest': true 16 | }, 17 | 'plugins': [ 18 | 'import', 19 | 'node', 20 | 'promise' 21 | ], 22 | 'rules': { 23 | 'arrow-parens': 'off', 24 | 'comma-dangle': [ 25 | 'error', 26 | 'only-multiline' 27 | ], 28 | 'complexity': ['error', 10], 29 | 'func-names': 'off', 30 | 'global-require': 'off', 31 | 'handle-callback-err': [ 32 | 'error', 33 | '^(err|error)$' 34 | ], 35 | 'import/no-unresolved': [ 36 | 'error', 37 | { 38 | 'caseSensitive': true, 39 | 'commonjs': true, 40 | 'ignore': ['^[^.]'] 41 | } 42 | ], 43 | 'import/prefer-default-export': 'off', 44 | 'linebreak-style': 'off', 45 | 'no-catch-shadow': 'error', 46 | 'no-continue': 'off', 47 | 'no-div-regex': 'warn', 48 | 'no-else-return': 'off', 49 | 'no-param-reassign': 'off', 50 | 'no-plusplus': 'off', 51 | 'no-shadow': 'off', 52 | 'no-multi-assign': 'off', 53 | 'no-underscore-dangle': 'off', 54 | 'node/no-deprecated-api': 'error', 55 | 'node/process-exit-as-throw': 'error', 56 | 'object-curly-spacing': [ 57 | 'error', 58 | 'never' 59 | ], 60 | 'operator-linebreak': [ 61 | 'error', 62 | 'after', 63 | { 64 | 'overrides': { 65 | ':': 'before', 66 | '?': 'before' 67 | } 68 | } 69 | ], 70 | 'prefer-arrow-callback': 'off', 71 | 'prefer-destructuring': 'off', 72 | 'prefer-template': 'off', 73 | 'quote-props': [ 74 | 1, 75 | 'as-needed', 76 | { 77 | 'unnecessary': true 78 | } 79 | ], 80 | 'semi': [ 81 | 'error', 82 | 'never' 83 | ], 84 | 'indent': ['error', 2], 85 | 'space-before-function-paren': ['error', 'never'], 86 | 'no-return-assign': 'off', 87 | 'complexity': 'off', 88 | 'no-use-before-define': 'off', 89 | 'max-len': 'off', 90 | 'no-restricted-syntax': 'off', 91 | 'no-console': 'off', 92 | 'class-methods-use-this': 'off', 93 | 'no-nested-ternary': 'off', 94 | 'no-mixed-operators': 'off', 95 | 'consistent-return': 'off', 96 | 'no-restricted-globals': 'off', 97 | 'promise/always-return': 'off', 98 | 'camelcase': 'off', 99 | 'no-control-regex': 'off', 100 | 'no-await-in-loop': 'off', 101 | }, 102 | 'globals': { 103 | 'window': true, 104 | 'document': true, 105 | 'App': true, 106 | 'Page': true, 107 | 'Component': true, 108 | 'Behavior': true, 109 | 'wx': true, 110 | 'getCurrentPages': true, 111 | } 112 | } -------------------------------------------------------------------------------- /src/depsAnalyzer/utils/genData.js: -------------------------------------------------------------------------------- 1 | const genPageCompData = (comps = [], componentDeps) => { 2 | let totalSize = 0 3 | const children = [] 4 | for (const comp of comps) { 5 | totalSize += componentDeps[comp].size 6 | const compName = comp.slice(comp.lastIndexOf('/') + 1) 7 | children.push({ 8 | name: compName, 9 | size: componentDeps[comp].size, 10 | absolutePath: comp 11 | }) 12 | } 13 | children.sort((a, b) => b.size - a.size) 14 | 15 | return { 16 | name: 'components', 17 | size: +totalSize.toFixed(2), 18 | children 19 | } 20 | } 21 | 22 | const genFileData = (file, allFileInfo) => { 23 | const fileInfo = allFileInfo[file] 24 | return { 25 | name: fileInfo.basename, 26 | size: fileInfo.size, 27 | absolutePath: file 28 | } 29 | } 30 | 31 | const genPageData = ({ 32 | name, pageDeps, componentDeps, allFileInfo 33 | }) => { 34 | const comps = pageDeps.compDeps 35 | const compData = genPageCompData(comps, componentDeps) 36 | 37 | const children = [] 38 | let totalSize = 0 39 | const files = pageDeps.files.sort() 40 | files.forEach(file => { 41 | const fileData = genFileData(file, allFileInfo) 42 | children.push(fileData) 43 | totalSize += fileData.size 44 | }) 45 | children.sort((a, b) => b.size - a.size) 46 | 47 | const pages = { 48 | name: 'pages', 49 | size: +totalSize.toFixed(2), 50 | children 51 | } 52 | 53 | return { 54 | name, 55 | size: +(pages.size + compData.size).toFixed(2), 56 | children: [pages, compData] 57 | } 58 | } 59 | 60 | const genModuleData = ({pages, componentDeps, allFileInfo}) => { 61 | const children = [] 62 | let pagesTotalSize = 0 63 | for (const page of Object.keys(pages)) { 64 | const pageData = genPageData({ 65 | name: page, 66 | componentDeps, 67 | allFileInfo, 68 | pageDeps: pages[page] 69 | }) 70 | children.push(pageData) 71 | pagesTotalSize += pageData.size 72 | } 73 | children.sort((a, b) => b.size - a.size) 74 | 75 | return { 76 | children, 77 | size: +pagesTotalSize.toFixed(2) 78 | } 79 | } 80 | 81 | const genData = ({dependencies, componentDeps, allFileInfo}) => { 82 | const {app, pages, subpackages} = dependencies 83 | const data = { 84 | app: { 85 | name: 'app', 86 | size: 0, 87 | children: [] 88 | }, 89 | pages: { 90 | name: 'pages', 91 | size: 0, 92 | children: [] 93 | }, 94 | subpackages: { 95 | name: 'subpackages', 96 | size: 0, 97 | children: [] 98 | } 99 | } 100 | 101 | const appData = genModuleData({ 102 | pages: {app}, 103 | componentDeps, 104 | allFileInfo 105 | }) 106 | 107 | data.app = appData.children[0] 108 | data.pages = Object.assign(data.pages, genModuleData({ 109 | pages, 110 | componentDeps, 111 | allFileInfo 112 | })) 113 | 114 | data.subpackages = Object.assign(data.subpackages, genModuleData({ 115 | pages: subpackages, 116 | componentDeps, 117 | allFileInfo 118 | })) 119 | 120 | // if (!data.subpackages.size) delete data.subpackages 121 | // if (!data.pages.size) delete data.pages 122 | // if (!data.app.size) delete data.app 123 | 124 | return data 125 | } 126 | 127 | module.exports = { 128 | genData 129 | } 130 | -------------------------------------------------------------------------------- /src/depsAnalyzer/utils/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const inspect = require('util').inspect 3 | const Table = require('cli-table3') 4 | const colors = require('colors') 5 | 6 | 7 | const createLog = (module) => { 8 | const manager = require('simple-node-logger').createLogManager({ 9 | timestampFormat: 'YYYY-MM-DD HH:mm:ss' 10 | }) 11 | const log = manager.createLogger(module) 12 | return log 13 | } 14 | 15 | const suffixExtname = (filePath, ext = 'js') => { 16 | const sep = path.sep 17 | const {dir, name} = path.parse(filePath) 18 | return dir ? `${dir}${sep}${name}.${ext}` : `${name}.${ext}` 19 | } 20 | 21 | const removeExtname = (filePath) => { 22 | const sep = path.sep 23 | const {dir, name} = path.parse(filePath) 24 | return dir ? `${dir}${sep}${name}` : `${name}` 25 | } 26 | 27 | const unique = (arr = []) => Array.from(new Set(arr)) 28 | 29 | const printObject = (Object) => { 30 | console.log(inspect(Object, {showHidden: false, depth: null})) 31 | } 32 | 33 | const genPackOptions = (unusedFiles, pluginRoot) => { 34 | const map = {} 35 | unusedFiles.forEach(file => { 36 | if (pluginRoot) { 37 | file = path.join(pluginRoot, file) 38 | } 39 | const ext = path.extname(file).replace('.', '') 40 | const name = removeExtname(file) 41 | if (!map[name]) map[name] = [] 42 | map[name].push(ext) 43 | }) 44 | const packOptions = {ignore: []} 45 | Object.keys(map).forEach(name => { 46 | const exts = map[name] 47 | if (exts.length === 1) { 48 | packOptions.ignore.push({ 49 | type: 'file', 50 | value: `${name}.${exts[0]}` 51 | }) 52 | } else if (exts.length > 1) { 53 | packOptions.ignore.push({ 54 | type: 'glob', 55 | value: `${name}.@(${exts.join('|')})` 56 | }) 57 | } 58 | }) 59 | return packOptions 60 | } 61 | 62 | const drawTable = (data) => { 63 | const items = [ 64 | ...data.pages.children, 65 | ...data.subpackages.children 66 | ] 67 | items.sort((a, b) => b.size - a.size) 68 | items.unshift(data.app) 69 | 70 | const table = new Table({ 71 | head: ['page', 'file & comp', 'stat size (kB)', 'percent', 'totalSize (kB)'], 72 | style: {} 73 | }) 74 | 75 | 76 | items.forEach(item => { 77 | const name = item.name 78 | const totalSize = item.size 79 | const pages = item.children[0].children || [] 80 | const components = item.children[1].children || [] 81 | const rows = pages.length + components.length 82 | table.push([{ 83 | rowSpan: rows, 84 | content: name, 85 | vAlign: 'center' 86 | }, { 87 | content: pages[0].absolutePath 88 | }, { 89 | content: pages[0].size 90 | }, { 91 | content: (pages[0].size / totalSize * 100).toFixed(2) + '%' 92 | }, { 93 | rowSpan: rows, 94 | content: totalSize, 95 | vAlign: 'center' 96 | }]) 97 | 98 | for (let i = 1; i < pages.length; i++) { 99 | table.push([{ 100 | content: pages[i].absolutePath 101 | }, { 102 | content: pages[i].size 103 | }, { 104 | content: (pages[i].size / totalSize * 100).toFixed(2) + '%' 105 | }]) 106 | } 107 | 108 | for (let i = 0; i < components.length; i++) { 109 | table.push([{ 110 | content: colors.green(components[i].absolutePath) 111 | }, { 112 | content: components[i].size 113 | }, { 114 | content: (components[i].size / totalSize * 100).toFixed(2) + '%' 115 | }]) 116 | } 117 | }) 118 | console.log(table.toString()) 119 | } 120 | 121 | module.exports = { 122 | suffixExtname, 123 | removeExtname, 124 | createLog, 125 | unique, 126 | printObject, 127 | genPackOptions, 128 | drawTable 129 | } 130 | -------------------------------------------------------------------------------- /src/depsAnalyzer/handler/esmodule.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | 3 | const fs = require('fs-extra') 4 | const parser = require('@babel/parser') 5 | const traverse = require('@babel/traverse').default 6 | const t = require('babel-types') 7 | const {suffixExtname} = require('../utils/util') 8 | const {findAbsolutePath} = require('./util') 9 | 10 | const singleModuleAnalyser = ({filePath, ext}) => { 11 | const code = fs.readFileSync(filePath, 'utf-8') 12 | const ast = parser.parse(code, {sourceType: 'module'}) 13 | const deps = {} 14 | traverse(ast, { 15 | CallExpression({node}) { 16 | const calleeName = node.callee.name 17 | if (calleeName === 'require') { 18 | const firstParam = node.arguments[0] 19 | if (t.isStringLiteral(firstParam)) { 20 | const depFilePath = findAbsolutePath({ 21 | filePath, 22 | relativePath: firstParam.value, 23 | ext 24 | }) 25 | if (depFilePath) { 26 | deps[firstParam.value] = depFilePath 27 | } 28 | } 29 | } 30 | }, 31 | 32 | ImportDeclaration({node}) { 33 | const depFilePath = findAbsolutePath({ 34 | filePath, 35 | relativePath: node.source.value, 36 | ext 37 | }) 38 | if (depFilePath) { 39 | deps[node.source.value] = depFilePath 40 | } 41 | } 42 | }) 43 | 44 | return { 45 | filePath, 46 | deps, 47 | } 48 | } 49 | 50 | const genModuleDepsGraph = ({entry, ext, stack = []}) => { 51 | entry = suffixExtname(entry, ext) 52 | if (!fs.existsSync(entry)) return {} 53 | if (stack.includes(entry)) return {} 54 | 55 | stack.push(entry) 56 | const entryModule = singleModuleAnalyser({filePath: entry, ext}) 57 | 58 | const deps = entryModule.deps 59 | const depsGraph = {[entry]: deps} 60 | Object.values(deps).forEach(entry => { 61 | Object.assign(depsGraph, genModuleDepsGraph({entry, ext, stack})) 62 | }) 63 | stack.pop() 64 | return depsGraph 65 | } 66 | 67 | class Node { 68 | constructor(path, id = 0, children = []) { 69 | this.id = id 70 | this.path = path 71 | this.children = children 72 | } 73 | 74 | addChild(id) { 75 | this.children.push(id) 76 | } 77 | } 78 | 79 | class Graph { 80 | constructor() { 81 | this.nodes = [] 82 | this.map = {} 83 | this.size = 0 84 | } 85 | 86 | addNode(node) { 87 | node.id = this.size 88 | this.map[node.path] = node.id 89 | this.nodes.push(node) 90 | this.size++ 91 | } 92 | } 93 | 94 | /** 95 | * 格式化成邻接表 96 | * @param {*} moduleDepsGraph 97 | */ 98 | const formatModuleDepsGraph = (moduleDepsGraph) => { 99 | const graph = new Graph() 100 | Object.keys(moduleDepsGraph).forEach(entry => { 101 | const node = new Node(entry) 102 | graph.addNode(node) 103 | }) 104 | Object.keys(moduleDepsGraph).forEach(entry => { 105 | const entryId = graph.map[entry] 106 | const deps = Object.values(moduleDepsGraph[entry]) 107 | deps.forEach(dep => { 108 | const id = graph.map[dep] 109 | graph.nodes[entryId].addChild(id) 110 | }) 111 | }) 112 | return graph 113 | } 114 | 115 | const genWxsModuleDepsGraph = (entry) => { 116 | const stack = [] 117 | const moduleDepsGraph = genModuleDepsGraph({entry, ext: 'wxs', stack}) 118 | const graph = formatModuleDepsGraph(moduleDepsGraph) 119 | return graph 120 | } 121 | 122 | const genEsModuleDepsGraph = (entry) => { 123 | const stack = [] 124 | const moduleDepsGraph = genModuleDepsGraph({entry, ext: 'js', stack}) 125 | const graph = formatModuleDepsGraph(moduleDepsGraph) 126 | return graph 127 | } 128 | 129 | module.exports = { 130 | genEsModuleDepsGraph, 131 | genWxsModuleDepsGraph 132 | } 133 | -------------------------------------------------------------------------------- /src/depsAnalyzer/utils/unused.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const fs = require('fs-extra') 4 | const {difference} = require('./setOperation') 5 | const {analyzeComponent, computeComponentSize} = require('../handler/analyzerComp') 6 | const {findAbsolutePath} = require('../handler/util') 7 | 8 | const defaultIgnores = [ 9 | '**/node_modules/**', 10 | 'project.config.json', 11 | '**/sitemap.json' 12 | ] 13 | 14 | const findAllComponent = () => { 15 | const jsonFiles = glob.sync('**/*.json', {ignore: [...defaultIgnores]}) 16 | const components = new Set() 17 | 18 | for (const filePath of jsonFiles) { 19 | const content = fs.readJSONSync(filePath) 20 | if (content.component === true) { 21 | components.add(filePath) 22 | } 23 | 24 | if (content.usingComponents) { 25 | const usingComps = Object.values(content.usingComponents) 26 | usingComps.forEach(comp => { 27 | if (!comp.startsWith('plugin://')) { 28 | const compPath = findAbsolutePath({ 29 | filePath, 30 | relativePath: comp, 31 | ext: 'json' 32 | }) 33 | if (compPath) components.add(compPath) 34 | } 35 | }) 36 | } 37 | } 38 | return Array.from(components) 39 | } 40 | 41 | const findAllFileInfo = () => { 42 | const exts = ['wxml', 'wxss', 'wxs', 'js', 'json'].join('|') 43 | const allFiles = glob.sync(`**/*.@(${exts})`, { 44 | ignore: [...defaultIgnores] 45 | }) 46 | const allFileInfo = {} 47 | allFiles.forEach(file => { 48 | const size = +(fs.statSync(file).size / 1000).toFixed(2) // kB 49 | const ext = path.extname(file) 50 | const name = path.basename(file, ext) 51 | const basename = path.basename(file) 52 | allFileInfo[file] = { 53 | name, 54 | ext, 55 | size, 56 | basename 57 | } 58 | }) 59 | return allFileInfo 60 | } 61 | 62 | const findIgnoreFils = (ignore = []) => { 63 | const ignoreFiles = [] 64 | ignore.forEach(pattern => { 65 | const files = glob.sync(pattern, {ignore: [...defaultIgnores]}) 66 | ignoreFiles.push(...files) 67 | }) 68 | const uniqueIgnoreFiles = Array.from(new Set(ignoreFiles)) 69 | return uniqueIgnoreFiles 70 | } 71 | 72 | const findAllComponentDeps = (allFileInfo) => { 73 | const components = findAllComponent() 74 | const componentDeps = {} 75 | components.forEach(comp => { 76 | const deps = analyzeComponent(comp) 77 | const size = computeComponentSize(deps, allFileInfo) 78 | deps.size = size 79 | componentDeps[comp] = deps 80 | }) 81 | return componentDeps 82 | } 83 | 84 | const findUnusedFiles = ({ 85 | allFileInfo, 86 | componentDeps, 87 | dependencies, 88 | ignore 89 | }) => { 90 | // 所有文件 91 | const allFiles = Object.keys(allFileInfo) 92 | const allFileSet = new Set(allFiles) 93 | 94 | const {app, pages, subpackages} = dependencies 95 | const deps = [app, ...Object.values(pages), ...Object.values(subpackages)] 96 | const usedCompSet = new Set() 97 | deps.forEach(item => { 98 | item.compDeps.forEach(comp => usedCompSet.add(comp)) 99 | }) 100 | usedCompSet.forEach(comp => { 101 | if (componentDeps[comp]) { 102 | deps.push(componentDeps[comp]) 103 | } 104 | }) 105 | // 未使用的组件 106 | // const allCompSet = new Set(components) 107 | // const unusedCompSet = difference(allCompSet, usedCompSet) 108 | 109 | // 已使用的文件 110 | const usedFiles = [] 111 | deps.forEach(item => { 112 | usedFiles.push(...item.files) 113 | }) 114 | const usedFileSet = new Set(usedFiles) 115 | const ignoreFiles = findIgnoreFils(ignore) 116 | const ignoreFileSet = new Set(ignoreFiles) 117 | 118 | // 未使用的文件 119 | let unusedFileSet = difference(allFileSet, usedFileSet) 120 | unusedFileSet = difference(unusedFileSet, ignoreFileSet) 121 | 122 | const unusedFiles = Array.from(unusedFileSet) 123 | return unusedFiles 124 | } 125 | 126 | module.exports = { 127 | findUnusedFiles, 128 | findAllComponentDeps, 129 | findAllFileInfo 130 | } 131 | -------------------------------------------------------------------------------- /test/minicode/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "setting": { 4 | "urlCheck": true, 5 | "es6": true, 6 | "postcss": true, 7 | "preloadBackgroundData": false, 8 | "minified": true, 9 | "newFeature": true, 10 | "coverView": true, 11 | "nodeModules": true, 12 | "autoAudits": false, 13 | "showShadowRootInWxmlPanel": true, 14 | "scopeDataCheck": false, 15 | "checkInvalidKey": true, 16 | "checkSiteMap": true, 17 | "uploadWithSourceMap": true, 18 | "babelSetting": { 19 | "ignore": [], 20 | "disablePlugins": [], 21 | "outputPath": "" 22 | }, 23 | "enhance": true 24 | }, 25 | "compileType": "miniprogram", 26 | "libVersion": "2.10.2", 27 | "appid": "wx407d0fba58912be6", 28 | "projectname": "miniprogram-bundle-analyzer", 29 | "debugOptions": { 30 | "hidedInDevtools": [] 31 | }, 32 | "isGameTourist": false, 33 | "simulatorType": "wechat", 34 | "simulatorPluginLibVersion": {}, 35 | "packOptions": { 36 | "ignore": [ 37 | { 38 | "type": "file", 39 | "value": "analyzer/result.json" 40 | }, 41 | { 42 | "type": "glob", 43 | "value": "components/comp.@(js|json|wxml)" 44 | }, 45 | { 46 | "type": "file", 47 | "value": "miniprogram_npm/eventemitter3/index.js" 48 | }, 49 | { 50 | "type": "glob", 51 | "value": "miniprogram_npm/weui-miniprogram/actionsheet/actionsheet.@(js|json|wxml|wxss)" 52 | }, 53 | { 54 | "type": "glob", 55 | "value": "miniprogram_npm/weui-miniprogram/badge/badge.@(js|json|wxml|wxss)" 56 | }, 57 | { 58 | "type": "glob", 59 | "value": "miniprogram_npm/weui-miniprogram/checkbox-group/checkbox-group.@(js|json|wxml|wxss)" 60 | }, 61 | { 62 | "type": "glob", 63 | "value": "miniprogram_npm/weui-miniprogram/checkbox/checkbox.@(js|json|wxml|wxss)" 64 | }, 65 | { 66 | "type": "glob", 67 | "value": "miniprogram_npm/weui-miniprogram/dialog/dialog.@(js|json|wxml|wxss)" 68 | }, 69 | { 70 | "type": "glob", 71 | "value": "miniprogram_npm/weui-miniprogram/form-page/form-page.@(js|json|wxml|wxss)" 72 | }, 73 | { 74 | "type": "glob", 75 | "value": "miniprogram_npm/weui-miniprogram/form/form.@(js|json|wxml)" 76 | }, 77 | { 78 | "type": "glob", 79 | "value": "miniprogram_npm/weui-miniprogram/gallery/gallery.@(js|json|wxml|wxss)" 80 | }, 81 | { 82 | "type": "glob", 83 | "value": "miniprogram_npm/weui-miniprogram/half-screen-dialog/half-screen-dialog.@(js|json|wxml|wxss)" 84 | }, 85 | { 86 | "type": "glob", 87 | "value": "miniprogram_npm/weui-miniprogram/icon/icon.@(js|json|wxml|wxss)" 88 | }, 89 | { 90 | "type": "glob", 91 | "value": "miniprogram_npm/weui-miniprogram/index.@(js|json)" 92 | }, 93 | { 94 | "type": "glob", 95 | "value": "miniprogram_npm/weui-miniprogram/msg/msg.@(js|json|wxml|wxss)" 96 | }, 97 | { 98 | "type": "glob", 99 | "value": "miniprogram_npm/weui-miniprogram/navigation-bar/navigation-bar.@(js|json|wxml|wxss)" 100 | }, 101 | { 102 | "type": "file", 103 | "value": "miniprogram_npm/weui-miniprogram/package.json" 104 | }, 105 | { 106 | "type": "glob", 107 | "value": "miniprogram_npm/weui-miniprogram/slideview/slideview.@(js|json|wxml|wxs|wxss)" 108 | }, 109 | { 110 | "type": "glob", 111 | "value": "miniprogram_npm/weui-miniprogram/tabbar/tabbar.@(js|json|wxml|wxss)" 112 | }, 113 | { 114 | "type": "glob", 115 | "value": "miniprogram_npm/weui-miniprogram/toptips/toptips.@(js|json|wxml|wxss)" 116 | }, 117 | { 118 | "type": "glob", 119 | "value": "miniprogram_npm/weui-miniprogram/uploader/uploader.@(js|json|wxml|wxss)" 120 | }, 121 | { 122 | "type": "glob", 123 | "value": "miniprogram_npm/weui-miniprogram/video-swiper/video-swiper.@(js|json|wxml|wxss)" 124 | }, 125 | { 126 | "type": "file", 127 | "value": "miniprogram_npm/wxml-to-canvas/utils.js" 128 | }, 129 | { 130 | "type": "file", 131 | "value": "package-lock.json" 132 | }, 133 | { 134 | "type": "file", 135 | "value": "package.json" 136 | }, 137 | { 138 | "type": "file", 139 | "value": "util/unused.js" 140 | } 141 | ] 142 | }, 143 | "condition": { 144 | "search": { 145 | "current": -1, 146 | "list": [] 147 | }, 148 | "conversation": { 149 | "current": -1, 150 | "list": [] 151 | }, 152 | "plugin": { 153 | "current": -1, 154 | "list": [] 155 | }, 156 | "game": { 157 | "currentL": -1, 158 | "list": [] 159 | }, 160 | "gamePlugin": { 161 | "current": -1, 162 | "list": [] 163 | }, 164 | "miniprogram": { 165 | "current": -1, 166 | "list": [ 167 | { 168 | "id": -1, 169 | "name": "subpackages/buy/buy", 170 | "pathName": "subpackages/buy/buy", 171 | "scene": null 172 | } 173 | ] 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /src/depsAnalyzer/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const ora = require('ora') 3 | const program = require('commander') 4 | const path = require('path') 5 | const shell = require('shelljs') 6 | const perf = require('execution-time')() 7 | const {genData} = require('./utils/genData') 8 | const {genPackOptions, drawTable} = require('./utils/util') 9 | const {findUnusedFiles, findAllComponentDeps, findAllFileInfo} = require('./utils/unused') 10 | const {analyzeComponent} = require('./handler/analyzerComp') 11 | const {genEsModuleDepsGraph} = require('./handler/esmodule') 12 | 13 | /** 14 | * 1. 忽略引用插件中的组件 15 | * 2. 当前目录为 project.config.json 目录 16 | */ 17 | const genAppDepsGraph = (cli) => { 18 | // log.setLevel('warn') 19 | 20 | const projectConfigPath = './project.config.json' 21 | if (!fs.existsSync(projectConfigPath)) { 22 | console.warn('Error: project.config.json is not exist') 23 | return 24 | } 25 | 26 | const projectConfig = fs.readJSONSync(projectConfigPath) 27 | const { 28 | compileType = 'miniprogram', 29 | miniprogramRoot = './', 30 | pluginRoot = 'plugin' 31 | } = projectConfig 32 | 33 | const cwd = process.cwd() 34 | const ignore = cli.ignore ? cli.ignore.split(',') : [] 35 | const showTable = Boolean(cli.table) 36 | const root = compileType === 'miniprogram' ? miniprogramRoot : pluginRoot 37 | 38 | // 所有的操作均在代码根目录进行 39 | shell.cd(root) 40 | const entryPath = compileType === 'miniprogram' ? 'app.json' : 'plugin.json' 41 | const entryJson = fs.readJSONSync(entryPath) 42 | const pages = compileType === 'miniprogram' ? entryJson.pages : Object.values(entryJson.pages || {}) 43 | 44 | const dependencies = { 45 | app: {}, 46 | pages: {}, 47 | subpackages: {} 48 | } 49 | 50 | perf.start('global') 51 | perf.start() 52 | const spinner = ora(`analyze ${entryPath}`).start() 53 | // app plugin 处理 54 | dependencies.app = analyzeComponent(entryPath) 55 | // 针对插件的特殊处理 56 | if (compileType === 'plugin') { 57 | const mainPath = entryJson.main || 'index.js' 58 | const esmoduleDepsGraph = genEsModuleDepsGraph(mainPath) 59 | dependencies.app.esDeps = Object.keys(esmoduleDepsGraph.map) 60 | dependencies.app.files.push(...dependencies.app.esDeps) 61 | } 62 | spinner.succeed(`analyze ${entryPath} success, used ${Math.ceil(perf.stop().time)}ms`) 63 | 64 | // 分包处理 65 | if (compileType === 'miniprogram') { 66 | perf.start() 67 | spinner.start('analyzer subpackages') 68 | 69 | const subpackages = entryJson.subpackages || entryJson.subPackages || [] 70 | subpackages.forEach(subpackage => { 71 | const {root, pages} = subpackage 72 | pages.forEach(page => { 73 | const entry = path.join(root, page) 74 | dependencies.subpackages[entry] = analyzeComponent(entry) 75 | }) 76 | }) 77 | 78 | spinner.succeed(`analyzer subpackages success, used ${Math.ceil(perf.stop().time)}ms`) 79 | } 80 | 81 | // 页面处理 82 | perf.start() 83 | spinner.start('analyzer pages') 84 | 85 | pages.forEach(page => { 86 | dependencies.pages[page] = analyzeComponent(page) 87 | }) 88 | spinner.succeed(`analyzer pages success, used ${Math.ceil(perf.stop().time)}ms`) 89 | 90 | // 无用文件 91 | perf.start() 92 | spinner.start('find unusedFiles') 93 | const allFileInfo = findAllFileInfo() 94 | const componentDeps = findAllComponentDeps(allFileInfo) 95 | const unusedFiles = findUnusedFiles({ 96 | allFileInfo, 97 | componentDeps, 98 | dependencies, 99 | ignore 100 | }) 101 | spinner.succeed(`find unusedFiles success, used ${Math.ceil(perf.stop().time)}ms`) 102 | 103 | // 生成打包配置 104 | perf.start() 105 | spinner.start('generate packOptions') 106 | const packOptions = genPackOptions(unusedFiles, compileType === 'plugin' ? pluginRoot : '') 107 | spinner.succeed(`generate packOptions success, used ${Math.ceil(perf.stop().time)}ms`) 108 | 109 | // 可视化数据 110 | perf.start() 111 | spinner.start('generate file size data') 112 | const data = genData({ 113 | dependencies, 114 | componentDeps, 115 | allFileInfo 116 | }) 117 | spinner.succeed(`generate file size data success, used ${Math.ceil(perf.stop().time)}ms`) 118 | 119 | const result = { 120 | packOptions, 121 | dependencies, 122 | unusedFiles, 123 | data 124 | } 125 | 126 | // 输出结果 127 | spinner.start('write output') 128 | shell.cd(cwd) 129 | const outputDir = cli.output 130 | const outputJsonFile = path.join(outputDir, 'result.json') 131 | fs.ensureDirSync(outputDir) 132 | fs.writeFileSync(outputJsonFile, JSON.stringify(result, null, 2)) 133 | 134 | if (cli.write) { 135 | if (!projectConfig.packOptions) projectConfig.packOptions = {} 136 | if (!projectConfig.packOptions.ignore) projectConfig.packOptions.ignore = [] 137 | 138 | const _packOptions = projectConfig.packOptions 139 | const _ignore = _packOptions.ignore 140 | _ignore.push(...packOptions.ignore) 141 | fs.writeFileSync('project.config.json', JSON.stringify(projectConfig, null, 2)) 142 | } 143 | spinner.succeed(`finish, everything looks good, total used ${Math.ceil(perf.stop('global').time)}ms`) 144 | 145 | if (showTable) { 146 | drawTable(data) 147 | } 148 | } 149 | 150 | program 151 | .command('analyzer') 152 | .description('Analyze dependencies of miniprogram, find out unused files') 153 | .option('-o, --output [dir]', 'path to directory for result', './analyzer') 154 | .option('-i, --ignore ', 'glob pattern for files what should be excluded from unused files') 155 | .option('-w, --write', 'overwrite old project.config.json') 156 | .option('-t, --table', 'print miniprogram file size data') 157 | .action(genAppDepsGraph) 158 | -------------------------------------------------------------------------------- /docs/deps.md: -------------------------------------------------------------------------------- 1 | # 依赖分析,查找无用文件 2 | 3 | 对小程序的页面和组件进行依赖分析,找出未被引用的文件,生成 `packOptions` 项,在开发者工具上传代码时忽略无用文件。 4 | 5 | 支持小程序/插件,仅对 `wxml`、`wxss`、`wxs`、`js`、`json` 以及组件进行分析,不包括组件内的图片等资源。 6 | 7 | 需要注意的是,`js` 文件的依赖,支持 `import` 和 `require` 导入的模块,但运行时计算的路径如 `require(a + b)` 将无法识别。 8 | 9 | ## 用法 10 | 11 | ```js 12 | Usage: miniprogram-slim analyzer [options] 13 | 14 | Analyze dependencies of miniprogram, find out unused files 15 | 16 | Options: 17 | -o, --output [dir] path to directory for result (default: "./analyzer") 18 | -i, --ignore glob pattern for files what should be excluded from unused files 19 | -w, --write overwrite old project.config.json 20 | -t, --table print miniprogram file size data 21 | -h, --help output usage information 22 | ``` 23 | 24 | 进入包含 `project.config.json` 的项目根目录,执行 `miniprogram-slim analyzer`,默认会生成 `./analyzer/result.json` 文件,记录生成的数据结果。 25 | 26 | ```json 27 | { 28 | "packOptions": { 29 | "ignore": [] 30 | }, 31 | "dependencies": { 32 | "app": { 33 | "esDeps": [], 34 | "wxmlDeps": [], 35 | "wxssDeps": [], 36 | "compDeps": [], 37 | "wxsDeps": [], 38 | "jsonDeps": [], 39 | "files": [] 40 | }, 41 | "pages": {}, 42 | "subpackages": {} 43 | }, 44 | "unusedFiles": [], 45 | "data": {} 46 | } 47 | ``` 48 | 49 | * `packOptions` 字段记录着在开发者工具打包上传时可以被忽略的文件,拷贝该部分至 `project.config.json` 即可,执行 `miniprogram-slim analyzer -w` 将自动进行同步。 50 | 51 | * `dependencies` 字段记录着文件间的依赖关系,按页面维护分割,包括与页面相关的 `wxml`、`wxss`、`js`、`wxs` 以及组件的引用。 52 | 53 | * `unusedFiles` 为未引用的文件数组。 54 | 55 | * `data` 为保持依赖关系的文件大小的集合,`test/minicode` 项目测试部分结果如下,其中后缀为 `.json` 的表示一个组件。 56 | 57 | 58 | ``` 59 | ┌─────────────────────────┬─────────────────────────────────────────────────────────────────┬────────────────┬─────────┬────────────────┐ 60 | │ page │ file & comp │ stat size (kB) │ percent │ totalSize (kB) │ 61 | ├─────────────────────────┼─────────────────────────────────────────────────────────────────┼────────────────┼─────────┼────────────────┤ 62 | │ │ miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss │ 46.54 │ 62.89% │ │ 63 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 64 | │ │ app.json │ 0.58 │ 0.78% │ │ 65 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 66 | │ │ app.js │ 0.08 │ 0.11% │ │ 67 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 68 | │ │ app.wxss │ 0.08 │ 0.11% │ │ 69 | │ app ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ 74 │ 70 | │ │ util/util-c.js │ 0.04 │ 0.05% │ │ 71 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 72 | │ │ miniprogram_npm/weui-miniprogram/cell/cell.json │ 11.35 │ 15.34% │ │ 73 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 74 | │ │ miniprogram_npm/weui-miniprogram/searchbar/searchbar.json │ 9.13 │ 12.34% │ │ 75 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 76 | │ │ miniprogram_npm/weui-miniprogram/cells/cells.json │ 6.2 │ 8.38% │ │ 77 | ├─────────────────────────┼─────────────────────────────────────────────────────────────────┼────────────────┼─────────┼────────────────┤ 78 | │ │ miniprogram_npm/miniprogram-sm-crypto/index.js │ 56.6 │ 55.85% │ │ 79 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 80 | │ │ miniprogram_npm/jsbn/index.js │ 43.76 │ 43.18% │ │ 81 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 82 | │ │ pages/other/other.js │ 0.89 │ 0.88% │ │ 83 | │ pages/other/other ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ 101.35 │ 84 | │ │ pages/other/other.wxml │ 0.05 │ 0.05% │ │ 85 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 86 | │ │ pages/other/other.json │ 0.03 │ 0.03% │ │ 87 | │ ├─────────────────────────────────────────────────────────────────┼────────────────┼─────────┤ │ 88 | │ │ pages/other/other.wxss │ 0.02 │ 0.02% │ │ 89 | ├─────────────────────────┼─────────────────────────────────────────────────────────────────┼────────────────┼─────────┼────────────────┤ 90 | ``` 91 | -------------------------------------------------------------------------------- /test/minicode/report/jscpd-report.html: -------------------------------------------------------------------------------- 1 | jscpd, Copy/Paste Detector
Total filesTotal lines of codeDuplicated lines% of duplications
410400%
--------------------------------------------------------------------------------