├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── package.json ├── src ├── classes │ ├── FileTree.js │ ├── Loader.js │ ├── ModuleHelper.js │ ├── OutPutPath.js │ └── Wxml.js ├── config │ └── constant.js ├── helpers │ ├── analyze-graph.js │ ├── calc-code-dep.js │ ├── calc-content-hash.js │ ├── create-resolver.js │ ├── get-files.js │ ├── html-mini-loader.js │ ├── merge-entry.js │ ├── module.js │ ├── normal-entrys.js │ ├── parse-entry.js │ ├── resolve-asset-content.js │ ├── resolve-component-path.js │ ├── resolve-dist-path.js │ ├── resolve-target-path.js │ ├── update-code.js │ └── wxml-parser.js ├── index.js ├── lib │ └── require.js ├── platform │ └── wx │ │ ├── get-empty-file-source.js │ │ └── wxml.js ├── plugin │ ├── FileEntryPlugin.js │ ├── MiniProgramPlugin.js │ └── MiniTemplatePlugin.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "loose": true, 6 | "targets": { 7 | "node": "6.10" 8 | } 9 | }] 10 | ] 11 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "standard", 3 | "globals": { 4 | "App": true, 5 | "Page": true 6 | }, 7 | "rules": { 8 | "no-return-await": [0] 9 | } 10 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 由于很久没有写小程序了,所以这个工具可能不维护了 2 | 3 | 4 | ## mini-program-webpack-loader 5 | 6 | 基于 webpack 4.0 的小程序打包工具。 7 | 8 | **项目依赖 async/await, Set/Map, spread 等 es6+ 语法** 9 | 10 | ## 安装 11 | 12 | ``` bash 13 | $ npm i mini-program-webpack-loader --dev 14 | ``` 15 | 16 | ## 介绍 17 | 18 | 该工具主要解决小程序难以集成更多的成熟工具的问题。其次支持多个小程序项目共建。 19 | 20 | 该工具由两部分组成,loader 和 plugin。 21 | 22 | ### 能力 23 | - 支持在小程序项目中使用 webpack 的所有能力 24 | - 支持在 wxml, wxss, wxs, json 文件中使用模块别名 25 | - 支持全局注册自定义组件 26 | - 支持多小程序项目合并 27 | - 支持小程序项目分析 28 | 29 | ### 插件 30 | 31 | #### 使用 32 | 33 | ``` javascript 34 | const MiniPlugin = require('mini-program-webpack-loader').plugin; 35 | 36 | module.exports = { 37 | ..., // webpack 其他设置 38 | plugins: [ 39 | new MiniPlugin({ 40 | ... // 参数 41 | }) 42 | ], 43 | ... // webpack 其他设置 44 | } 45 | ``` 46 | 47 | #### 参数 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 80 | 83 | 84 | 85 | 86 | 89 | 92 | 93 | 94 | 95 | 98 | 99 | 102 | 103 | 104 | 107 | 110 | 111 | 112 | 113 | 116 | 119 | 120 | 121 | 122 | 125 | 126 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 156 | 159 | 162 | 163 | 164 | 167 | 170 | 173 | 174 | 175 | 178 | 181 | 184 | 185 | 186 | 189 | 192 | 195 | 196 | 197 | 200 | 201 | 204 | 205 | 206 | 209 | 212 | 213 | 214 | 215 | 218 | 221 | 222 | 223 | 224 |
extfile`true`打包主包下的 ext.json(默认值)
`false` 58 | 不打包 ext.json 59 |
`String` 64 | extfile 文件路径 65 |
69 | `ignoreTabbar` 70 |
72 | `Boolean` 73 |
是否把 tabbar 中的图片添加到构建,考虑到很多场景除了 tabbar 资源,可能还存在其他资源不能被插件索引到,可以通过 copy 插件复制资源,所以插件默认不会构建 tabbar 依赖的图片内容
78 | `silently` 79 |
81 | `Boolean` 82 |
是否输出构建信息,默认 `false`,输出构建信息
87 | `optimizeMainPackage` 88 |
90 | `Boolean` 91 |
是否优化主包体积。在一些场景下,组件只在多个分包使用,于是组件只能放到主包内,插件提供配置,自动拷贝这些组件到分包内,以减小主包体积,默认值为 `true`
96 |

setSubPackageCacheGroup(miniLoader, appJson)

97 |
`Function` 100 |

根据最后输出的 `app.json` 设置 `cacheGroup`

101 |
105 | `useFinalCallback` 106 |
108 | `Boolean` 109 |
是否使用自定义的构建完成回调,默认使用插件内置的回调来输出构建信息。
114 | `compilationFinish(err, stat, appJson)` 115 |
117 | `Function` 118 |
打包完成后回调
123 |

resources

124 |
`Array` 127 |

提供资源的目录。

128 |

除了所有入口所在的目录,src目录,node_modules,其他目录需要在这里添加否则可能导致路径计算错误。

129 |

130 |
如 131 | `path/to/src/pages/one/index.json`依赖了一个绝对路径 132 | ` 133 | path/to/shared/conponents/List/index.json` 134 | 135 |

136 |

其中 137 | `path/to/src`为项目目录, 138 | `path/to/shared` 139 | 为多个项目公用的目录。

140 |

141 |
则必须设置 142 | `resources: ['path/to/shared']`。则最终打包会把 143 | path/to/shared/conponents 和 144 | path/to/src/pages 输出到同级目录。 145 |

146 |
entry`Object`每个 key 必须为 webpack 对应的 entry 配置的绝对路径。值为一个对象。
154 | `entry.accept` 155 |
157 | `Object` 158 |
160 |

accept 会从对应的入口配置中读取对应的字段,进行保留。即如果 entry 中设置了入口文件配置,则不在 accept 中的字段,都会被直接删除。

161 |
165 | `entry.accept[property]` 166 |
168 | `any` 169 |
171 |

对于非特殊说明的字段,因为对应入口有了配置就会删除不在 accept 对应中的字段,如果希望保留其中部分字段可以通过设置对应 key 的值为 `true`

172 |
176 | `entry.accept.pages` 177 |
179 | `Array` | `true` 180 |
182 |

如果值是数组,则会从当前入口文件的 `pages` 字段获取对应的页面,其他页面会被丢弃。`true` 值会保留所有的页面,配合 `ignore.pages` 可以丢弃其中部分不用的页面

183 |
187 | `entry.accept.usingComponents` 188 |
190 | `Array` | `true` 191 |
193 |

如果值是数组,元素的值应该是入口文件的 `usingConponents` 字段对应的key,表示要保留的组件,不在数组中的其他组件会被丢弃。`true` 值会保留所有的组件,配合 `ignore.usingComponents` 可以丢弃其中部分不用的组件

194 |
198 | `entry.ignore` 199 |
`Object` 202 |

ignore 配置用于删除通过 accept 保留的配置。目前仅支持 pages。

203 |
207 | `entry.ignore.pages` 208 |
210 | `Array` 211 |
可以删除 pages 和 subpackages 里面的页面
216 | `entry.ignore.usingComponents` 217 |
219 | `Array` 220 |
不加载对应入口文件 `usingConponents` 字段对应组件
225 | 226 | 关于插件的其他介绍可以访问 [这里](https://github.com/realywithoutname/mini-program-webpack-loader/wiki/%E5%85%B3%E4%BA%8E-loader) 227 | 228 | ### Loader 229 | 关于 loader 的其他介绍可以访问 [这里](https://github.com/realywithoutname/mini-program-webpack-loader/wiki/%E5%85%B3%E4%BA%8E-loader) 230 | 231 | 关于 loader 的配置可以查看 [这个示例](https://github.com/realywithoutname/mini-loader-plugin-demo/blob/master/build/webpack.config.base.loaders.js) 232 | 233 | ### 关于多项目共建 234 | 在这里共建的意思是:多个小程序项目的功能共用。其中包括页面,组件,工具函数的共用。 235 | 236 | #### 页面共用 237 | 通过 webpack 的 entry 设置多个 json 配置文件,插件根据这些文件进行解析依赖的页面和组件。对于不需要的配置可以通过插件配置来进行管理。 238 | 239 | ```JavaScript 240 | module.exports = { 241 | entry: [ 242 | 'path/dir-one/src/app.json', 243 | 'path/dir-two/src/app.json' 244 | ], 245 | ..., 246 | plugins: [ 247 | new MiniPlugin({}) 248 | ], 249 | ... 250 | } 251 | ``` 252 | 在有多个不同的小程序项目,我们称第一个入口为主入口,像 ext.json 这样的文件将从从这个主入口对应的目录进行读取。 253 | 254 | #### 组件共用 255 | 组件共用主要借用 webpack 的 resolve.alias 的能力,在开发中我们只需要在 webpack 配置中设置相应的配置,即可在代码中使用绝对的路径加载文件。 256 | 下面以使用 `path/dir-two` 这个项目中的 `base-component` 组件为例,展示如何在另外个项目中使用它。 257 | 258 | ``` JavaScript 259 | const DIR_TWO = resolve(__dirname, 'DIR_TWO') 260 | module.exports = { 261 | entry: [ 262 | 'path/dir-one/app.json' 263 | ], 264 | resolve: { 265 | alias: { 266 | 'project-two': DIR_TWO 267 | } 268 | }, 269 | ..., 270 | plugins: [ 271 | new MiniPlugin({ 272 | // 这个配置即可保证 `dir-two` 目录下的文件正确的输出到希望输出的目录中 273 | resources: [ 274 | DIR_TWO 275 | ] 276 | }) 277 | ], 278 | ... 279 | } 280 | ``` 281 | 282 | 在需要使用这个组件的地方使用即可 283 | 284 | ``` json 285 | { 286 | "usingComponents": { 287 | "base-component": "project-two/path/to/project-two/index" 288 | } 289 | } 290 | ``` 291 | 292 | ### 辅助方法 293 | - `moduleOnlyUsedBySubpackages(module): Boolean` 查询模块是否只在子包中使用 294 | - `moduleUsedBySubpackage(module, root): Boolean` 查询模块是否在特定子包中使用 295 | - `moduleOnlyUsedBySubPackage(module, root): Boolean` 查询模块是否只在特定子包使用 296 | - `pathInSubpackage(path): Boolean` 查询指定路径是否在子包中 297 | - `getAssetContent(file, compilation): String` 获取某个文件的内容 298 | - `getAppJson(): Object` 获取最终的 app.json 内容 299 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-program-webpack-loader", 3 | "version": "1.2.9", 4 | "description": "mini-program webpack loader", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "npx babel src --out-dir lib", 8 | "test": "echo \"just demo\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "webpack", 12 | "mini-program" 13 | ], 14 | "author": "liujiandong", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "eslint": "^5.6.1", 18 | "eslint-config-standard": "^12.0.0", 19 | "eslint-plugin-import": "^2.14.0", 20 | "eslint-plugin-node": "^7.0.1", 21 | "eslint-plugin-promise": "^4.0.1", 22 | "eslint-plugin-standard": "^4.0.0", 23 | "webpack": "^4.9.1" 24 | }, 25 | "dependencies": { 26 | "@babel/generator": "^7.12.11", 27 | "@babel/parser": "^7.12.11", 28 | "@babel/traverse": "^7.12.12", 29 | "@babel/types": "^7.12.12", 30 | "colors": "^1.3.0", 31 | "console.table": "^0.10.0", 32 | "domhandler": "^2.3.0", 33 | "enhanced-resolve": "^4.1.0", 34 | "htmlparser2": "^3.9.2", 35 | "lebab": "^3.0.3", 36 | "querystring": "^0.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/classes/FileTree.js: -------------------------------------------------------------------------------- 1 | const { basename, dirname } = require('path') 2 | const { resolveTargetPath } = require('../helpers/resolve-target-path') 3 | const { relative, join } = require('../utils') 4 | const { getFile } = require('../helpers/get-files') 5 | 6 | /** 7 | FileNode: { 8 | source: String, // 文件绝对路径 9 | dist: String, // 文件输出路径 10 | deps: Set([FileNode]), // 文件依赖的文件 11 | used: Set([FileNode]), // 文件被依赖的文件(使用到这个文件) 12 | isXXX: Boolean, // 文件类型 13 | components: Map([componentName, path]), // json 文件才有的,组件名和组件的路径 14 | generics: Map(), // 暂时没用 15 | } 16 | 17 | PageNode: { 18 | isSub: Boolean, // 是不是分包 19 | files: Set([FileNode]) // 页面依赖的文件 20 | } 21 | 22 | ComponentNode: { 23 | files: Set([FileNode]), // 自定义组件依赖的文件 24 | used: Set([file]), // 自定义组件被使用的文件 25 | type: Map([file, type]), // 依赖的自定义组件的类型,在输出时需要用到 26 | json: Function => fileMeta // 方便快速的找到依赖文件中的 json 文件 27 | } 28 | */ 29 | 30 | const set = (target, key, val) => { 31 | target[key] = val 32 | return target 33 | } 34 | 35 | let regRules = { 36 | '.js$': meta => set(meta, 'isJs', true), 37 | '.json$': meta => set(meta, 'isJson', true) && 38 | set(meta, 'components', new Map()) && 39 | set(meta, 'generics', new Map()), 40 | '.wxml$': meta => set(meta, 'isWxml', true) && set(meta, 'exteralClasses', new Set()), 41 | '.wxs$': meta => set(meta, 'isWxs', true), 42 | '.wxss$': meta => set(meta, 'isWxss', true), 43 | '.scss$': meta => set(meta, 'isScss', true), 44 | '.pcss$': meta => set(meta, 'isPcss', true), 45 | '.less$': meta => set(meta, 'isLess', true) 46 | // '.less$': meta => set(meta, 'isLess', true), 47 | } 48 | 49 | function getFileMeta (file, outputUtil) { 50 | let meta = { 51 | source: file, 52 | dist: resolveTargetPath( 53 | outputUtil.get(file) 54 | ), 55 | deps: new Set(), 56 | used: new Set() 57 | } 58 | 59 | meta.updateHash = function (hash) { 60 | meta.hash = hash 61 | } 62 | 63 | meta.clone = function () { 64 | return { ...meta } 65 | } 66 | 67 | for (const key in regRules) { 68 | if (new RegExp(`${key}`).test(getFile(file))) { 69 | regRules[key].call(null, meta) 70 | break 71 | } 72 | } 73 | 74 | return meta 75 | } 76 | 77 | class FileTree { 78 | constructor (miniLoader) { 79 | this.tree = new Map() 80 | this.tree.set('entry', new Set()) 81 | this.tree.set('pages', new Map()) 82 | this.tree.set('files', new Map()) 83 | this.tree.set('components', new Map()) 84 | this.tree.set('globalComponents', new Map()) 85 | 86 | this.outputMap = {} 87 | this.miniLoader = miniLoader 88 | this.id = 0 89 | } 90 | 91 | get size () { 92 | return this.files.size 93 | } 94 | 95 | /** 96 | * 普通文件结构 97 | * Map { 98 | * path: { 99 | * source: FilePath, 100 | * deps: FileSet, 101 | * [FILE TYPE]: true 102 | * } 103 | * } 104 | */ 105 | get files () { 106 | return this.tree.get('files') 107 | } 108 | 109 | get pageSize () { 110 | return this.pages.size 111 | } 112 | 113 | get subPageSize () { 114 | let pageMap = this.pages 115 | let size = 0 116 | 117 | for (const page of pageMap.values()) { 118 | if (page.isSub) { 119 | size++ 120 | } 121 | } 122 | 123 | return size 124 | } 125 | 126 | /** 127 | * pages Map { 128 | * page: { 129 | * isSub: true/false, 130 | * files: FileSet 131 | * } 132 | * } 133 | */ 134 | get pages () { 135 | return this.tree.get('pages') 136 | } 137 | 138 | get comSize () { 139 | return this.components.size 140 | } 141 | 142 | /** 143 | * path: { 144 | * source: FilePath, 145 | * isJson: true, 146 | * components: Map {tag: FilePath }, 147 | * files: FileSet, // 有完整的链 148 | * used: Set { FilePath } // 该文件被其他哪些文件引用 149 | * } 150 | */ 151 | get components () { 152 | return this.tree.get('components') 153 | } 154 | 155 | get entry () { 156 | return this.tree.get('entry') 157 | } 158 | 159 | addEntry (entry, files) { 160 | this.entry.has(entry) && this.clearFiles( 161 | this.getFile(entry) 162 | ) 163 | 164 | if (!this.entry.has(entry)) { 165 | this.setFile(entry, null, true) 166 | const entryMeta = this.getFile(entry) 167 | 168 | const depFiles = this.setFile(files, entryMeta) 169 | this.tree.set(entry, entryMeta) 170 | 171 | entryMeta.files = depFiles 172 | this.entry.add(entry) 173 | } 174 | } 175 | 176 | /** 177 | * @param {*} pagePath 178 | * @param {*} pageFiles 179 | * @param {*} isSubPkg 180 | */ 181 | addPage (pagePath, pageFiles, inSubPkg, independent, entry) { 182 | let pagesMap = this.pages 183 | this.clearFiles(pagesMap.get(pagePath)) 184 | /** 185 | * 页面不存在被使用 186 | */ 187 | let pageFileSet = this.setFile(pageFiles, null, false, independent) 188 | 189 | pageFileSet.forEach(meta => { 190 | meta.isPageFile = true 191 | }) 192 | pagesMap.set(pagePath, { 193 | isSub: inSubPkg, 194 | files: pageFileSet 195 | }) 196 | } 197 | 198 | /** 199 | * json 文件树结构 200 | * @param {*} file 201 | * @param {*} tag 202 | * @param {*} componentPath 组件的绝对路径 203 | * @param {*} componentFiles 204 | * 自定义组件 json 文件 205 | * { 206 | * source: file, 207 | * dist: resolveTargetPath( 208 | * outputUtil.get(file) 209 | * ), 210 | * deps: new Set(), 211 | * used: new Set(), 212 | * isJson: true, 213 | * components: new Map([componentName, componentAbsolutePath]), 214 | * generics: new Map 215 | * } 216 | * 217 | * Component 结构 218 | * { 219 | * files: Set([fileMeta]), 220 | * used: Set([file]), 221 | * type: Map([file, type]), 222 | * json: Function => fileMeta 223 | * } 224 | */ 225 | addComponent (file, tag, componentPath, componentFiles, type) { 226 | const fileMap = this.files 227 | const fileMeta = fileMap.get(file) 228 | const { components, independent } = fileMeta 229 | const entry = this.tree.get('entry') 230 | /** 231 | * 自定义组件只会被页面和自定义组件使用,不会被 app 使用 232 | */ 233 | const componentFileSet = this.setFile(componentFiles, entry.has(file) ? null : fileMeta, false, independent) 234 | let component = this.components.get(componentPath) 235 | 236 | this.clearFiles(component) 237 | 238 | if (!component) { 239 | component = { 240 | files: new Set(), 241 | used: new Set(), 242 | type: new Map(), 243 | get json () { 244 | const jsonFile = componentFiles.find(item => /\.json/.test(item)) 245 | return fileMap.get(jsonFile) 246 | } 247 | } 248 | this.components.set(componentPath, component) 249 | } 250 | 251 | componentFileSet.forEach(meta => { 252 | meta.isComponentFile = true 253 | }) 254 | 255 | component.type.set(file, type) 256 | component.files = componentFileSet 257 | component.used.add(file) 258 | 259 | // 全局自定义组件不需要放在 json 文件上,应该单独管理,否则会导致解析后的入口 json 文件内容不对 260 | if (entry.has(file)) { 261 | this.tree.get('globalComponents').set(tag, componentPath) 262 | return 263 | } 264 | 265 | components.set(tag, componentPath) 266 | } 267 | 268 | addGlobalComponent (file, tag, json) { 269 | const component = this.components.get(json) 270 | const componentFiles = [...component.files].map(({ source }) => source) 271 | this.addComponent(file, tag, json, componentFiles, 'normal') 272 | } 273 | 274 | /** 275 | * 一个自定义组件(页面)依赖的所有自定义组件 276 | * @param {*} jsonFile 277 | * @param {*} usedComponents 278 | */ 279 | addFullComponent (jsonFile, usedComponents) { 280 | const meta = this.getFile(jsonFile) 281 | meta.usedComponents = usedComponents 282 | 283 | usedComponents.forEach(tag => { 284 | if (!meta.components.has(tag)) { 285 | let originPath = this.tree.get('globalComponents').get(tag) 286 | 287 | if (!originPath) { 288 | return 289 | } 290 | 291 | this.addGlobalComponent( 292 | jsonFile, 293 | tag, 294 | originPath 295 | ) 296 | } 297 | }) 298 | } 299 | 300 | /** 301 | * @param {*} file 302 | * @param {*} depFiles 303 | */ 304 | addDeps (file, deps) { 305 | let fileMap = this.files 306 | let fileMeta = fileMap.get(file) 307 | const depMetas = new Set() 308 | 309 | for (const item of deps) { 310 | const meta = this.setFile(item.sourcePath, fileMeta, item.ignore) 311 | 312 | meta.forEach(meta => { 313 | // 一个文件可能被多个地方引用,在每个文件中使用的路径不同 314 | meta.depPath = meta.depPath || new Map() 315 | meta.depPath.set(file, item.origin) 316 | if (/\.wxml$/.test(meta.source)) meta.isTemplate = true 317 | }) 318 | 319 | depMetas.add(...meta) 320 | } 321 | 322 | fileMeta.deps = depMetas 323 | } 324 | 325 | has (file) { 326 | return this.files.has(file) 327 | } 328 | 329 | hasPage (page) { 330 | return this.pages.has(page) 331 | } 332 | 333 | /** 334 | * 添加文件 335 | * @param {*} files 要添加的文件 336 | * @param {*} user 文件使用者 337 | * @param {*} ignore 是否检查输出路径 338 | * @param {*} independent 是否在独立分包 339 | */ 340 | setFile (files, user, ignore, independent, dist) { 341 | let fileMap = this.files 342 | let fileSet = new Set() 343 | 344 | files = Array.isArray(files) ? files : [ files ] 345 | 346 | for (const file of files) { 347 | if (fileMap.has(file)) { 348 | const meta = fileMap.get(file) 349 | 350 | fileSet.add(meta) 351 | user && meta.used.add(user) 352 | continue 353 | } 354 | 355 | let meta = getFileMeta(file, this.miniLoader.outputUtil) 356 | let distFile = dist || this.outputMap[meta.dist] 357 | 358 | user && meta.used.add(user) 359 | meta.id = ++this.id 360 | meta.independent = independent 361 | 362 | // 允许引用不同的 node_modules 下的文件 363 | if (!ignore && distFile && (!/node_modules/.test(distFile) || !/node_modules/.test(file))) { 364 | throw new Error(` 365 | 项目存在不同文件输出到一个文件的情况: 366 | ${this.outputMap[meta.dist]} 以及 ${file} 367 | `) 368 | } 369 | 370 | this.outputMap[meta.dist] = file 371 | 372 | fileSet.add(meta) 373 | fileMap.set(file, meta) 374 | } 375 | 376 | return fileSet 377 | } 378 | 379 | getFile (file, safe) { 380 | let fileMap = this.files 381 | let fileMeta = fileMap.get(file) 382 | 383 | if (!fileMeta) { 384 | if (!safe) { 385 | throw new Error('Can`t find file: ' + file) 386 | } 387 | 388 | this.setFile(file) 389 | 390 | return this.files.get(file) 391 | } 392 | 393 | return fileMeta 394 | } 395 | 396 | getFileByDist (dist) { 397 | if (this.outputMap[dist]) { 398 | dist = this.outputMap[dist] 399 | } 400 | 401 | return this.getFile(dist, true) 402 | } 403 | 404 | getCanUseComponents (jsonFile, dist) { 405 | let usingComponents = new Map() 406 | let components = null 407 | 408 | // 文件本身定义的自定义组件 409 | let fileMeta = this.getFile(jsonFile, true) 410 | components = fileMeta.components 411 | 412 | const merge = (components) => { 413 | for (const [tag, path] of components) { 414 | if (usingComponents.has(tag)) continue 415 | /** 416 | * 抽象组件不存在对应的地址 417 | */ 418 | if (!path) { 419 | usingComponents.set(tag, { 420 | type: 'generics', 421 | distPath: true, 422 | originPath: '' 423 | }) 424 | 425 | continue 426 | } 427 | 428 | const { type, json: componentJson } = this.components.get(path) 429 | const conponentType = type.get(jsonFile) 430 | const config = { 431 | originPath: path, 432 | type: conponentType 433 | } 434 | 435 | if (conponentType === 'plugin') { 436 | config.distPath = path 437 | } else { 438 | const relPath = relative(dist, componentJson.dist) 439 | config.distPath = './' + join( 440 | dirname(relPath), 441 | basename(relPath, '.json') 442 | ) 443 | } 444 | 445 | usingComponents.set(tag, config) 446 | } 447 | } 448 | 449 | merge(components) 450 | 451 | // const globalComponents = this.tree.get('globalComponents') 452 | 453 | // merge(globalComponents) 454 | 455 | return usingComponents 456 | } 457 | 458 | clearFiles (depConfig = { files: new Set() }) { 459 | // TODO 清理依赖的文件 460 | } 461 | 462 | removeFile (file) { 463 | let fileMap = this.files 464 | let fileMeta = fileMap.get(file) 465 | 466 | fileMap.delete(file) 467 | 468 | return fileMeta 469 | } 470 | 471 | clearDepComponents (file) { 472 | let fileMap = this.files 473 | let { components } = fileMap.get(file) 474 | 475 | components.clear() 476 | } 477 | 478 | get wxmls () { 479 | let wxmls = [] 480 | 481 | for (const { files } of this.pages.values()) { 482 | for (const fileMeta of files) { 483 | fileMeta.isWxml && wxmls.push(fileMeta.source) 484 | } 485 | } 486 | 487 | for (const { files } of this.components.values()) { 488 | for (const fileMeta of files) { 489 | fileMeta.isWxml && wxmls.push(fileMeta.source) 490 | } 491 | } 492 | 493 | return wxmls 494 | } 495 | 496 | get jsons () { 497 | let jsons = [] 498 | 499 | for (const { files } of this.pages.values()) { 500 | for (const fileMeta of files) { 501 | fileMeta.isJson && jsons.push(fileMeta.source) 502 | } 503 | } 504 | 505 | for (const { files } of this.components.values()) { 506 | for (const fileMeta of files) { 507 | fileMeta.isJson && jsons.push(fileMeta.source) 508 | } 509 | } 510 | 511 | return jsons 512 | } 513 | 514 | get wxs () { 515 | let wxs = [] 516 | 517 | for (const fileMeta of this.files.values()) { 518 | fileMeta.isWxs && wxs.push(fileMeta.source) 519 | } 520 | 521 | return wxs 522 | } 523 | } 524 | 525 | module.exports = FileTree 526 | -------------------------------------------------------------------------------- /src/classes/Loader.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs') 2 | const path = require('path') 3 | const { LOADER_ACCEPT_FILE_EXTS } = require('../config/constant') 4 | const minisizeWxml = require('../helpers/html-mini-loader') 5 | const isInvaildExt = ext => LOADER_ACCEPT_FILE_EXTS.indexOf(ext) === -1 6 | 7 | class MiniLoader { 8 | constructor (loader, code) { 9 | this.loader = loader 10 | this.source = code 11 | this.callback = loader.async() 12 | this.context = loader.context 13 | 14 | if (!this.miniLoader) throw new Error('该 loader 必须和插件配合使用') 15 | 16 | this.fileTree = this.miniLoader.fileTree 17 | // 获取文件信息 18 | this.fileMeta = this.fileTree.getFile(loader.resourcePath) 19 | 20 | this.resolve = (context, request) => new Promise((resolve, reject) => { 21 | loader.resolve(context, request, (err, result) => err ? reject(err) : resolve(result)) 22 | }) 23 | 24 | // wxml 压缩 25 | if (this.fileMeta.isWxml) { 26 | this.source = minisizeWxml(code, this.fileMeta) 27 | } 28 | 29 | /** 30 | * 返回最终这个文件的内容 31 | */ 32 | this.normalParser().then( 33 | code => this.callback(null, code), 34 | this.callback 35 | ) 36 | } 37 | 38 | async normalParser (reg) { 39 | return this.loadNormalFileDeps(reg).then(map => { 40 | let deps = [] 41 | let code = this.source 42 | let promises = [] 43 | 44 | for (const value of map.values()) { 45 | /** 46 | * 动态添加依赖,使用 promise 是为了在所有依赖添加完成后 47 | * 再调用 callback,否则后添加的依赖不会被监听 48 | */ 49 | promises.push( 50 | this.addDepsModule(value.sourcePath) 51 | ) 52 | 53 | deps.push(value) 54 | } 55 | 56 | /** 57 | * 依赖的文件添加到文件树中 58 | */ 59 | this.fileTree.addDeps(this.fileMeta.source, deps) 60 | 61 | return Promise.all(promises).then(() => code) 62 | }) 63 | } 64 | 65 | addDepsModule (request) { 66 | return new Promise((resolve, reject) => { 67 | this.loader.loadModule(request, (err, src) => { 68 | if (!err) return resolve(src) 69 | // 如果添加依赖失败,把他从文件树中去除 70 | reject(err) 71 | this.fileTree.removeFile(request) 72 | }) 73 | }) 74 | } 75 | 76 | async loadNormalFileDeps () { 77 | let map = new Map() 78 | let { isWxml, isWxss, isWxs } = this.fileMeta 79 | 80 | if (!isWxml && !isWxs && !isWxss) { 81 | console.log('webpack 配置不对哦,该插件只支持 wxml, wxss, wxs, json 的'.red) 82 | return map 83 | } 84 | 85 | /** 86 | * 根据文件类型获取依赖匹配的正则表达式 87 | */ 88 | const wxmlDepsReg = /src=('|")([^"]*)('|")/g 89 | const wxssDepsReg = /@import ('|")([^"].+?)('|");/g 90 | const wxsDepsReg = /require\(('|")([^)]*.wxs)('|")\)/g 91 | const reg = isWxml ? wxmlDepsReg : isWxss ? wxssDepsReg : wxsDepsReg 92 | 93 | let matched = null 94 | 95 | /** 96 | * 依赖查找 97 | */ 98 | while ((matched = reg.exec(this.source)) !== null) { 99 | let dep = matched[2] 100 | let ext = path.extname(dep) 101 | 102 | /** 103 | * 检查文件引用的文件是否有效 104 | */ 105 | if (isInvaildExt(ext)) { 106 | // 可以在这里对很多东西限制,比如 base 64 107 | // console.log('引用了一个不认识的文件类型', ext) 108 | continue 109 | } 110 | 111 | // 依赖文件的绝对路径 112 | let depFile = await this.getAbsolutePath(this.context, dep) 113 | 114 | if (!map.has(dep)) { 115 | map.set(dep, { 116 | origin: dep, // 原来代码中的依赖路径 117 | sourcePath: depFile // 依赖文件,用于动态添加依赖 118 | }) 119 | } 120 | } 121 | 122 | return map 123 | } 124 | 125 | async getAbsolutePath (context, dep) { 126 | /** 127 | * 绝对路径则把前面的 / 去掉,需要在 resolve.alias 中做相应配置,主要是兼容有赞小程序历史写法,相对路径则使用相对路径 128 | * 129 | * 如果配置的 alias 和 / 后面的第一个目录不是指向同一个目录,这里获取到的路径就是错了 130 | */ 131 | dep = path.isAbsolute(dep) ? dep.substr(1) : dep 132 | 133 | let absPath = await this.resolve(context, dep) 134 | return absPath 135 | } 136 | } 137 | 138 | module.exports = MiniLoader 139 | 140 | module.exports.$applyPluginInstance = function (plugin) { 141 | MiniLoader.prototype.miniLoader = plugin 142 | } 143 | -------------------------------------------------------------------------------- /src/classes/ModuleHelper.js: -------------------------------------------------------------------------------- 1 | const { dirname, basename, extname, join } = require('path') 2 | 3 | module.exports = class ModuleHelper { 4 | constructor (miniLoader) { 5 | this.miniLoader = miniLoader 6 | this.fileTree = miniLoader.fileTree 7 | this.fileEntryPlugin = miniLoader.FileEntryPlugin 8 | this.chunks = new Map() 9 | this.deps = new Map() 10 | } 11 | 12 | /** 13 | * 给模块添加使用者,记录这个模块被哪些 chunk 使用 14 | * @param {*} module 15 | * @param {*} dep 16 | * @param {*} chunkName 17 | */ 18 | addDep (chunkName, dep) { 19 | let deps = this.chunks.get(chunkName) 20 | if (!deps) { 21 | deps = new Set() 22 | this.chunks.set(chunkName, deps) 23 | } 24 | 25 | let used = this.deps.get(dep) 26 | 27 | if (!used) { 28 | used = new Set() 29 | this.deps.set(dep, used) 30 | } 31 | 32 | used.add(chunkName) 33 | deps.add(dep) 34 | } 35 | 36 | tree () { 37 | let tree = {} 38 | 39 | for (const [key, val] of this.chunks) { 40 | tree[key] = [...val] 41 | } 42 | 43 | return tree 44 | } 45 | 46 | toUsed () { 47 | let tree = {} 48 | 49 | for (const [key, val] of this.deps) { 50 | tree[key] = [...val] 51 | } 52 | 53 | return tree 54 | } 55 | 56 | toJson () { 57 | return { 58 | chunks: this.tree(), 59 | deps: this.toUsed() 60 | } 61 | } 62 | 63 | getJsonFile (file) { 64 | const dir = dirname(file) 65 | const ext = extname(file) 66 | const name = basename(file, ext) 67 | 68 | return join(dir, `${name}.json`) 69 | } 70 | 71 | isComponentFile (file) { 72 | const jsonFile = this.getJsonFile(file) 73 | const component = this.fileTree.components.get(jsonFile) 74 | 75 | return !!component 76 | } 77 | 78 | moduleOnlyUsedBySubPackage (module, root) { 79 | const file = module.resource 80 | if (!/\.js$/.test(file) || module.isEntryModule()) return false 81 | if (!module._usedModules) return false 82 | 83 | const users = module._usedModules 84 | 85 | if (!users.size) return false 86 | 87 | const reg = new RegExp(`^${root}`) 88 | 89 | return !Array.from(users).some((source) => !reg.test(source)) 90 | } 91 | 92 | onlyUsedInSubPackagesReturnRoots (file) { 93 | const { packages } = this.fileEntryPlugin 94 | // 如果只有一个且是主包,直接跳过 95 | const firstPackage = packages[0] || {} 96 | if (Object.keys(packages).length === 1 && !firstPackage.root) { 97 | return [] 98 | } 99 | 100 | const fileMeta = this.fileTree.getFile(file) 101 | const { used } = fileMeta 102 | const roots = [] 103 | 104 | /** 105 | * 对于没有被使用的自定义组件应该被删除 106 | */ 107 | if (!used.size && fileMeta.isComponentFile) { 108 | return roots 109 | } 110 | 111 | // 假设该自定义组件只是在分包使用 112 | let usedInMainPackage = false 113 | for (const meta of used) { 114 | const stack = [meta] 115 | // 标记使用这个文件的单个文件是不是在分包。如果是在分包则该文件也在这个分包使用 116 | let usedInSubPackages = false 117 | while (stack.length && !usedInMainPackage) { 118 | const meta = stack.pop() 119 | const { source, used } = meta 120 | const matched = this.fileIsInSubPackage(source) // source.match(reg) 121 | if (matched) { 122 | roots.push(matched) 123 | usedInSubPackages = true 124 | continue 125 | } 126 | /** 127 | * 没有被使用的自定义组件(全局自定义组件),可以不用管 128 | * 设置 usedInSubPackages 为 true 129 | */ 130 | if (!used.size && meta.isComponentFile) { 131 | usedInSubPackages = true 132 | continue 133 | } else if (!used.size) { 134 | // 非自定义组件文件,没有被使用,说明是顶级文件,一定在主包 135 | usedInMainPackage = true 136 | break 137 | } 138 | 139 | /** 140 | * 其他如果 used 还有则继续向上判断 141 | * 如果 used.size 为 0,则表示页面,页面则如果是分包会被 match 到 142 | */ 143 | stack.unshift(...used) 144 | usedInSubPackages = false 145 | } 146 | 147 | // 如果有不在分包中使用,则表示在主包有使用 148 | if (!usedInSubPackages) { 149 | usedInMainPackage = true 150 | break 151 | } 152 | } 153 | 154 | return !usedInMainPackage && [...new Set(roots)] 155 | } 156 | 157 | /** 158 | * 文件在子包 159 | * @param {*} file 160 | */ 161 | fileIsInSubPackage (file) { 162 | const dist = this.miniLoader.outputUtil.get(file) 163 | const { packages } = this.fileEntryPlugin 164 | const reg = new RegExp( 165 | Object.keys(packages).filter(root => !!root).map(root => `/${root}/`).join('|') 166 | ) 167 | 168 | const matched = file.match(reg) 169 | 170 | if (matched) { 171 | const packName = matched[0].substring(1, matched[0].length - 1) 172 | const matchedPack = packages[packName] 173 | 174 | // 检查文件确实属于分包 175 | if (matchedPack && dist.indexOf(packName) === 0) { 176 | return matched[0].substr(1) 177 | } 178 | } 179 | return false 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/classes/OutPutPath.js: -------------------------------------------------------------------------------- 1 | const { orderSource } = require('../helpers/resolve-dist-path') 2 | const { join, relative, isAbsolute } = require('path') 3 | const { getFile } = require('../helpers/get-files') 4 | 5 | module.exports = class OutPutPath { 6 | constructor (context, resources, outPath) { 7 | this.sourceSet = orderSource(resources) 8 | this.outputPath = outPath 9 | this.compilerContext = context 10 | } 11 | 12 | get (path) { 13 | let fullPath = this.compilerContext 14 | let npmReg = /node_modules/g 15 | 16 | path = getFile(path) 17 | 18 | if (path === this.outputPath) return path 19 | 20 | path = path.replace(/(\.\.\/)?/g, ($1) => $1 ? '_/' : '') 21 | 22 | if (isAbsolute(path)) { 23 | fullPath = path 24 | } else { 25 | // 相对路径:webpack 最好生成的路径,打包入口外的文件都以 '_' 表示上级目录 26 | let pDirReg = /_\//g 27 | /** 28 | * 在 context 目录外的文件,会被处理为 _/_/ 这样的路径 29 | * 这里把以 _/ 开头的替换为 ../ 形式以计算输出路径。 30 | * pDirReg.lastIndex === 2 判断是以 _/ 开头,否则会存在如果目录名为 xxx_/xxx.xx 的时候也会被匹配,导致输出异常 31 | */ 32 | 33 | while (pDirReg.test(path) && pDirReg.lastIndex === 2) { 34 | path = path.substr(pDirReg.lastIndex) 35 | pDirReg.lastIndex = 0 36 | fullPath = join(fullPath, '../') 37 | } 38 | 39 | if (fullPath !== this.compilerContext) { 40 | fullPath = join(fullPath, path) 41 | } 42 | } 43 | 44 | if (fullPath !== this.compilerContext) { 45 | for (let index = 0; index < this.sourceSet.length; index++) { 46 | const source = this.sourceSet[index] 47 | let outPath = relative(source, fullPath).replace(/\\/g, '/') 48 | 49 | if (outPath && outPath.indexOf('..') === -1) { 50 | path = outPath 51 | console.assert(!npmReg.test(path), `文件${path}路径错误:不应该还包含 node_modules`) 52 | break 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * 如果有 node_modules 字符串,则去模块名称 59 | * 如果 app.json 在 node_modules 中,那 path 不应该包含 node_modules 60 | */ 61 | 62 | if (npmReg.test(path)) { 63 | path = path.substr(npmReg.lastIndex + 1) 64 | } 65 | 66 | return path 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/classes/Wxml.js: -------------------------------------------------------------------------------- 1 | const target = process.env.TARGET || 'wx' 2 | const { DomUtils, parseDOM } = require('htmlparser2') 3 | const { ConcatSource } = require('webpack-sources') 4 | const { resolveTargetPath } = require('../helpers/resolve-target-path') 5 | const { resolveAssetContent } = require('../helpers/resolve-asset-content') 6 | const { loadWxmlContent, isNativeTag } = require(`../platform/${target}/wxml`) 7 | 8 | module.exports = class Xml { 9 | constructor (miniLoader, compilation, request, dist) { 10 | this.dist = dist // 如果支持修改输出路径后,dist 就不能通过通过内部计算 11 | this.request = request 12 | this.compilation = compilation 13 | this.miniLoader = miniLoader 14 | 15 | this.getDistPath = src => miniLoader.outputUtil.get( 16 | resolveTargetPath(src) 17 | ) 18 | 19 | this.buff = loadWxmlContent(compilation, miniLoader.fileTree.getFile.bind(miniLoader.fileTree), request) 20 | } 21 | 22 | get dom () { 23 | if (this._dom) return this._dom 24 | 25 | let content = this.buff.source().toString() 26 | this._dom = parseDOM(content, { 27 | recognizeSelfClosing: true, 28 | lowerCaseAttributeNames: false 29 | }) 30 | 31 | return this._dom 32 | } 33 | 34 | usedComponents () { 35 | let tags = [] 36 | DomUtils.find((el) => { 37 | let { name, attribs = {} } = el 38 | 39 | // 记录所有非原生组件名 40 | if (name && !isNativeTag(name)) { 41 | tags.push(name) 42 | } 43 | 44 | let attrKeys = Object.keys(attribs) 45 | 46 | /** 47 | * 使用自定义组件是抽象组件 48 | */ 49 | if (/generic:/.test(attrKeys.join(';'))) { 50 | attrKeys.forEach(key => { 51 | /generic:/.test(key) && tags.push(attribs[key]) 52 | }) 53 | } 54 | }, this.dom, true) 55 | 56 | return tags 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config/constant.js: -------------------------------------------------------------------------------- 1 | module.exports.PROGRAM_ACCEPT_FILE_EXTS = ['.wxml', '.wxss', '.scss', '.pcss', '.less', '.js', '.json', '.wxs'] 2 | module.exports.ENTRY_ACCEPT_FILE_EXTS = ['.wxml', '.wxss', '.scss', '.pcss', '.less', '.js', '.json'] 3 | module.exports.LOADER_ACCEPT_FILE_EXTS = ['.wxml', '.wxss', '.scss', '.pcss', '.less', '.wxs'] 4 | -------------------------------------------------------------------------------- /src/helpers/analyze-graph.js: -------------------------------------------------------------------------------- 1 | module.exports.analyzeGraph = function analyzeGraph (stat, compilation) { 2 | let chunks = compilation.chunks 3 | 4 | chunks.forEach(chunk => { 5 | if (chunk.name === '__assets_chunk_name__0') { 6 | // console.log(chunk) 7 | } 8 | // console.log(chunk.name, chunk.getModules().length) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/calc-code-dep.js: -------------------------------------------------------------------------------- 1 | const target = process.env.TARGET || 'wx' 2 | const { ConcatSource } = require('webpack-sources') 3 | const { basename, dirname } = require('path') 4 | const { relative, join } = require('../utils') 5 | const { updateJsCode } = require('./update-code') 6 | const { resolveTargetPath } = require('./resolve-target-path') 7 | const { isNativeTag } = require(`../platform/${target}/wxml`) 8 | 9 | /** 10 | * 重写文件的依赖 11 | */ 12 | module.exports.calcCodeDep = function calcCodeDep (miniLoader, dist, meta, codeSource, checkAndCalcDep) { 13 | const source = new ConcatSource() 14 | 15 | let { isWxml, isWxss, isWxs, isJson, isJs, components, usedComponents = [], deps } = meta 16 | 17 | if (!(isWxml || isWxss || isWxs || isJson || isJs)) { 18 | return codeSource 19 | } 20 | 21 | let code = codeSource.source().toString() 22 | 23 | /** 24 | * 更新 js 中的 require 25 | */ 26 | if (isJs) { 27 | return updateJsCode(codeSource, meta.dist, dist) 28 | } 29 | 30 | /** 31 | * 小程序文件处理 32 | */ 33 | if ((isWxml || isWxss || isWxs) && deps.size) { 34 | for (const dep of deps) { 35 | const reg = `('|")${dep.depPath.get(meta.source)}('|")` 36 | const relativePath = resolveTargetPath( 37 | checkAndCalcDep ? checkAndCalcDep(dep.source) : relative(dist, dep.dist) 38 | ) 39 | code = code.replaceAll(reg, `"${relativePath}"`) 40 | } 41 | } 42 | 43 | /** 44 | * json 文件处理,主要处理自定义组件的 json 文件 45 | */ 46 | if (isJson && components.size) { 47 | code = JSON.parse(code) 48 | 49 | const canUseComponents = miniLoader.fileTree.getCanUseComponents(meta.source, dist) 50 | const { usingComponents = {}, componentGenerics = {}, component } = code 51 | 52 | /** 53 | * 统计定义未使用的组件 54 | */ 55 | const definedComponents = [ 56 | ...Object.keys(usingComponents), 57 | ...Object.keys(componentGenerics) 58 | ].reduce((res, key) => { 59 | res[key] = 0 60 | return res 61 | }, {}) 62 | /** 63 | * 统计未定义的组件 64 | */ 65 | const ignoredComponents = [] 66 | for (const componentName of usedComponents) { 67 | const component = canUseComponents.get(componentName) 68 | 69 | delete definedComponents[componentName] 70 | 71 | if (!component) { 72 | ignoredComponents.push(componentName) 73 | continue 74 | } 75 | 76 | // 插件使用插件地址 77 | if (component.type === 'plugin') { 78 | usingComponents[componentName] = component.distPath 79 | continue 80 | } 81 | 82 | // 无默认值抽象组件 83 | if (component.type === 'generics' && component.distPath === true) { 84 | componentGenerics[componentName] = true 85 | continue 86 | } 87 | 88 | /** 89 | * 普通自定义组件和有默认值的抽象组件,先计算依赖的相对路径 90 | * 如果有自定义的计算方法,则使用自定义计算方法。 91 | */ 92 | let distPath = checkAndCalcDep && checkAndCalcDep(component.originPath) 93 | 94 | // 自定义计算方法只是给了相对路径,需要去掉 .json 95 | distPath = !distPath ? component.distPath : ( 96 | './' + join( 97 | dirname(distPath), 98 | basename(distPath, '.json') 99 | ) 100 | ) 101 | 102 | if (component.type !== 'generics') { 103 | usingComponents[componentName] = distPath 104 | } else { 105 | componentGenerics[componentName] = { 106 | default: distPath 107 | } 108 | } 109 | } 110 | 111 | const definedAndNotUsed = Object.keys(definedComponents) 112 | 113 | definedAndNotUsed.forEach(componentName => { 114 | if (isNativeTag(componentName)) { 115 | miniLoader.compilation.errors.push( 116 | new Error(`${dist} 定义了原生组件 ${componentName},修改后重试`) 117 | ) 118 | } 119 | usingComponents[componentName] && delete usingComponents[componentName] 120 | componentGenerics[componentName] && delete componentGenerics[componentName] 121 | }) 122 | 123 | if (process.env.NODE_ENV !== 'production') { 124 | miniLoader.pushUndefinedTag(meta.source, ignoredComponents) 125 | miniLoader.pushDefinedNotUsedTag(meta.source, definedAndNotUsed) 126 | !component && miniLoader.pushUnDeclareComponentTag(meta.source) 127 | } 128 | /** 129 | * 有些自定义组件一开始没有定义 componentGenerics,usingComponents 130 | */ 131 | !code.usingComponents && Object.keys(usingComponents).length && (code.usingComponents = usingComponents) 132 | !code.componentGenerics && Object.keys(componentGenerics).length && (code.componentGenerics = componentGenerics) 133 | 134 | code = JSON.stringify(code, null, 2) 135 | } 136 | 137 | source.add(code) 138 | 139 | return source 140 | } 141 | -------------------------------------------------------------------------------- /src/helpers/calc-content-hash.js: -------------------------------------------------------------------------------- 1 | const { util: { createHash } } = require('webpack') 2 | const { RawSource } = require('webpack-sources') 3 | 4 | module.exports.getContentHash = function getContentHash (compilation, source) { 5 | const { outputOptions } = compilation 6 | const { 7 | hashFunction, 8 | hashDigest, 9 | hashDigestLength 10 | } = outputOptions 11 | 12 | const hash = createHash(hashFunction) 13 | 14 | if (source && typeof source.source) { 15 | source = new RawSource(source.source()) 16 | } 17 | 18 | source.updateHash(hash) 19 | 20 | return hash 21 | .digest(hashDigest) 22 | .substring(0, hashDigestLength) 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/create-resolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 插件中使用的 resolver,获取真实路径 3 | */ 4 | const { 5 | NodeJsInputFileSystem, 6 | CachedInputFileSystem, 7 | ResolverFactory 8 | } = require('enhanced-resolve') 9 | 10 | module.exports.createResolver = function (compiler) { 11 | const resolver = ResolverFactory.createResolver( 12 | Object.assign( 13 | { 14 | fileSystem: new CachedInputFileSystem(new NodeJsInputFileSystem(), 4000), 15 | extensions: ['.js', '.json'] 16 | }, 17 | compiler.options.resolve 18 | ) 19 | ) 20 | 21 | return (context, request) => { 22 | return new Promise((resolve, reject) => { 23 | resolver.resolve({}, context, request, {}, (err, res) => err ? reject(err) : resolve(res)) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/get-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取小程序路径对应的文件列表 3 | * @param {*} base 4 | * @param {*} path 5 | * @param {*} exts 6 | */ 7 | const { join } = require('path') 8 | const { existsSync } = require('fs') 9 | const EXTS = ['.js', '.ts', '.json', '.wxml', '.wxss', '.wxs', '.scss', '.pcss', '.less'] 10 | 11 | exports.getFiles = (base, path = '', exts) => { 12 | let files = [] 13 | 14 | path = join(base, path) 15 | 16 | for (const ext of (exts || EXTS)) { 17 | let file = path + ext 18 | if (existsSync(file)) files.push(file) 19 | } 20 | 21 | return files 22 | } 23 | 24 | exports.getFile = (request) => { 25 | const requestIndex = request.lastIndexOf('!') 26 | 27 | if (requestIndex !== -1) { 28 | request = request.substr(requestIndex + 1) 29 | } 30 | return request.split('?')[0] 31 | } 32 | -------------------------------------------------------------------------------- /src/helpers/html-mini-loader.js: -------------------------------------------------------------------------------- 1 | const DomHandler = require('domhandler') 2 | const { DomUtils, Parser } = require('htmlparser2') 3 | const ElementType = require('domelementtype') 4 | 5 | function trimWhitespace (str) { 6 | return str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, '') 7 | } 8 | 9 | function parseDOM (data, options) { 10 | const handler = new (DomHandler.DomHandler || DomHandler)(options) 11 | const parser = new Parser(handler, options) 12 | 13 | parser.onattribend = function () { 14 | if (this._cbs.onattribute) { this._cbs.onattribute(this._attribname, this._attribvalue) } 15 | if ( 16 | this._attribs && 17 | !Object.prototype.hasOwnProperty.call(this._attribs, this._attribname) 18 | ) { 19 | this._attribs[this._attribname] = this._attribvalue 20 | } 21 | this._attribname = '' 22 | this._attribvalue = '' // 没有值的属性不会触发 onattribdata 事件,就不会有 EMPTY_ATTRI_VALUE 值,最后在替换掉这个值 23 | } 24 | 25 | parser.onattribdata = function (value) { 26 | if (this._attribvalue === '') { 27 | this._attribvalue = 'EMPTY_ATTRI_VALUE' 28 | } 29 | 30 | this._attribvalue += value 31 | } 32 | 33 | parser.end(data) 34 | return handler.dom 35 | } 36 | 37 | module.exports = function (content, fileMeta) { 38 | let dom = parseDOM(content, { 39 | recognizeSelfClosing: true, 40 | lowerCaseAttributeNames: false, 41 | xmlMode: true 42 | }) 43 | 44 | const stack = [{ children: dom }] 45 | 46 | while (stack.length) { 47 | const node = stack.pop() 48 | const { children = [], type, data, attribs = {} } = node 49 | 50 | if (type === ElementType.Comment) { 51 | let parent = node.parent 52 | if (parent === null) { 53 | parent = { children: dom } 54 | } 55 | const index = parent.children.indexOf(node) 56 | if (index === -1) throw Error('...') 57 | 58 | parent.children.splice(index, 1) 59 | } 60 | 61 | if (type === ElementType.Text) { 62 | node.data = trimWhitespace(data) 63 | // wxml 文本存在 <= 符号,会被解释为开始进入一个标签,解析会出错,如 {{ a <= 444 }},只是小于符号不会有问题 64 | if (/{{/.test(data) && node.next && node.next.name === '=') { 65 | console.log(`\n${fileMeta.dist} 文件中存在 [<=] 符号,文件不会被压缩`) 66 | return content 67 | } 68 | } 69 | 70 | Object.keys(attribs).forEach(key => { 71 | // 属性没有值会被处理为 "",在小程序中表示 false,所以这里需要手动设置为true 72 | if (attribs[key] === '') { 73 | attribs[key] = `{{ true }}` 74 | } else { 75 | attribs[key] = attribs[key].replace(`EMPTY_ATTRI_VALUE`, '') 76 | } 77 | 78 | if (/"/.test(attribs[key])) { 79 | console.log('') 80 | console.log( 81 | fileMeta.dist, 82 | `文件中 ${node.name} 元素的 ${key} 属性 ${attribs[key]} 包含["]` 83 | ) 84 | } 85 | 86 | if (/\\/.test(attribs[key])) { 87 | console.log('') 88 | console.log( 89 | fileMeta.dist, 90 | `文件中 ${node.name} 元素的 ${key} 属性 ${attribs[key]} 包含[\\]` 91 | ) 92 | } 93 | }) 94 | 95 | if (!children.length) continue 96 | 97 | for (const child of children) { 98 | stack.push(child) 99 | } 100 | } 101 | return DomUtils.getInnerHTML({ children: dom }, { xmlMode: true }) 102 | } 103 | -------------------------------------------------------------------------------- /src/helpers/merge-entry.js: -------------------------------------------------------------------------------- 1 | module.exports.mergeEntrys = function (entrysCode, mainEntry) { 2 | let code = { 3 | preloadRule: {}, 4 | plugins: {}, 5 | subPackages: [] 6 | } 7 | 8 | for (const key in entrysCode) { 9 | if (entrysCode.hasOwnProperty(key)) { 10 | const rowCode = entrysCode[key] 11 | const isMain = key === mainEntry 12 | const { preloadRule, plugins = {} } = rowCode 13 | 14 | Object.assign(code.preloadRule, preloadRule) 15 | 16 | /** 17 | * 插件 18 | */ 19 | Object.keys(plugins).forEach((key) => { 20 | if (code.plugins[key]) { 21 | if (code.plugins[key].version !== plugins[key].version) { 22 | console.log(`插件 ${key} 在 ${key} 中使用了和其他入口不同的版本`.yellow) 23 | } 24 | return 25 | } 26 | code.plugins[key] = plugins[key] 27 | }) 28 | 29 | /** 30 | * 保证优先使用主入口文件的配置 31 | */ 32 | Object.keys(rowCode).forEach(key => { 33 | if (['preloadRule', 'plugins'].indexOf(key) === -1) { 34 | code[key] = isMain ? rowCode[key] : code[key] || rowCode[key] 35 | } 36 | }) 37 | } 38 | } 39 | 40 | return code 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/module.js: -------------------------------------------------------------------------------- 1 | const { get: getAppJson } = require('./app') 2 | 3 | module.exports.moduleOnlyUsedBySubpackages = function (module) { 4 | if (!/\.js$/.test(module.resource) || module.isEntryModule()) return false 5 | if (!module._usedModules) throw new Error('非插件提供的 module,不能调用这个方法') 6 | 7 | let { subPackages } = getAppJson() 8 | let subRoots = subPackages.map(({ root }) => root) || [] 9 | let subReg = new RegExp(subRoots.join('|')) 10 | let usedFiles = Array.from(module._usedModules) 11 | 12 | return !usedFiles.some(moduleName => !subReg.test(moduleName)) 13 | } 14 | 15 | module.exports.moduleUsedBySubpackage = function (module, root) { 16 | if (!/\.js$/.test(module.resource) || module.isEntryModule()) return false 17 | if (!module._usedModules) throw new Error('非插件提供的 module,不能调用这个方法') 18 | 19 | let reg = new RegExp(root) 20 | 21 | let usedFiles = Array.from(module._usedModules) 22 | 23 | return usedFiles.some(moduleName => reg.test(moduleName)) 24 | } 25 | 26 | module.exports.moduleOnlyUsedBySubPackage = function (module, root) { 27 | if (!/\.js$/.test(module.resource) || module.isEntryModule()) return false 28 | 29 | let usedFiles = module._usedModules 30 | 31 | if (!usedFiles) return false 32 | 33 | let reg = new RegExp(`^${root}`) 34 | 35 | return !Array.from(usedFiles).some(moduleName => !reg.test(moduleName)) 36 | } 37 | 38 | /** 39 | * 判断所给的路径在不在自定义组件内 40 | * @param {String} path 任意路径 41 | */ 42 | module.exports.pathInSubpackage = function (path) { 43 | let { subPackages } = getAppJson() 44 | 45 | for (const { root } of subPackages) { 46 | let match = path.match(root) 47 | 48 | if (match !== null && match.index === 0) { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | 56 | /** 57 | * 判断所给的路径集合是不是在同一个包内 58 | * @param {Array} paths 路径列表 59 | */ 60 | module.exports.pathsInSamePackage = function (paths) { 61 | // 取第一个路径,获取子包 root,然后和其他路径对比 62 | let firstPath = paths[0] 63 | let root = this.getPathRoot(firstPath) 64 | 65 | // 路径不在子包内 66 | if (!root) { 67 | return '' 68 | } 69 | 70 | let reg = new RegExp(`^${root}`) 71 | for (const path of paths) { 72 | if (!reg.test(path)) return '' 73 | } 74 | 75 | return root 76 | } 77 | 78 | /** 79 | * 判断列表内数据是不是在同一个目录下 80 | * @param {*} paths 81 | */ 82 | module.exports.pathsInSameFolder = function (paths) { 83 | let firstPath = paths[0] 84 | let folder = firstPath.split('/')[0] 85 | let reg = new RegExp(`^${folder}`) 86 | 87 | for (const path of paths) { 88 | if (!reg.test(path)) return '' 89 | } 90 | 91 | return folder 92 | } 93 | -------------------------------------------------------------------------------- /src/helpers/normal-entrys.js: -------------------------------------------------------------------------------- 1 | const { join, isAbsolute } = require('path') 2 | const { existsSync } = require('fs') 3 | 4 | /** 5 | * 标准化入口 6 | * @param {any} entry webpack config entry 7 | * 1. entry: 'path/entry.json' => ['path/entry.json'] 8 | * 2. entry: [ 'path/entry1.json', 'path/entry2.json', 'path/index.js' ] => [ 'path/entry1.json', 'path/entry2.json' ] 9 | * 3. entry: { app1: 'path/entry1.json', app2: 'path/entry2.json', index: 'path/index.js' } => [ 'path/entry1.json', 'path/entry2.json' ] 10 | * @param {Array} chunkNames 被忽略的 chunk 11 | */ 12 | exports.normalEntry = function normalEntry ( 13 | context = process.cwd(), 14 | entry = [] 15 | ) { 16 | let miniEntrys = [] 17 | let normalEntrys // 非小程序入口的 entry,如果没有则需要用小程序的entry 18 | 19 | let getEntry = (entry) => { 20 | entry = isAbsolute(entry) ? entry : join(context, entry) 21 | if (!existsSync(entry)) throw new Error('找不到文件:', entry) 22 | 23 | return entry 24 | } 25 | 26 | if (Array.isArray(entry)) { 27 | const mainEntrys = [] 28 | entry.forEach((item) => { 29 | if (/\.json/.test(item)) { 30 | miniEntrys.push(getEntry(item)) 31 | } else { 32 | mainEntrys.push(item) 33 | } 34 | }) 35 | /** 36 | * 存在其他entry时,添加到编译 37 | */ 38 | if (mainEntrys.length) { 39 | normalEntrys = { main: [...miniEntrys] } 40 | } 41 | } else if (typeof entry === 'object' && entry !== null) { 42 | Object.keys(entry).forEach((key) => { 43 | const _entry = Array.isArray(entry[key]) ? entry[key] : [entry[key]] 44 | 45 | let entrys = _entry.filter((entry) => { 46 | if (/\.json/.test(entry)) { 47 | miniEntrys.push(getEntry(entry)) 48 | return false 49 | } 50 | return true 51 | }) 52 | /** 53 | * 把其他非 json 的entry添加到编译 54 | */ 55 | if (entrys.length) { 56 | normalEntrys = normalEntrys || {} 57 | normalEntrys[key] = entrys 58 | } 59 | }) 60 | } 61 | 62 | if (typeof entry === 'string') { 63 | if (/\.json/.test(entry)) { 64 | miniEntrys = [entry] 65 | } 66 | } 67 | 68 | if (!miniEntrys.length) throw new Error('找不到一个有效的入口文件') 69 | 70 | return { 71 | miniEntrys: [...new Set(miniEntrys)], 72 | normalEntrys 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/helpers/parse-entry.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | function validateProptery (val, property) { 4 | if (val !== true && !Array.isArray(val)) { 5 | throw Error(`${property} 只接受 true 和数组`) 6 | } 7 | } 8 | function parsePackages (acceptPages = [], acceptPkg = [], ignorePages = [], config) { 9 | validateProptery(acceptPages, 'entry.accept.pages') 10 | validateProptery(acceptPkg, 'entry.accept.subPackages') 11 | 12 | const independentPkgs = (config.subPackages || []).filter(({ independent }) => independent) 13 | 14 | if (acceptPages === true) acceptPages = config.pages 15 | 16 | let roots = (config.subPackages || []).map(({ root }) => root) 17 | 18 | if (acceptPkg === true) acceptPkg = roots 19 | 20 | config.pages = acceptPages.filter(page => { 21 | if (config.pages.indexOf(page) !== -1) return true 22 | 23 | console.log(`page ${page} 在 pages 字段中不存在`.yellow) 24 | }) 25 | 26 | config.subPackages = acceptPkg.reduce((res, root) => { 27 | let index = roots.indexOf(root) 28 | if (index === -1) { 29 | console.log(`subPackages root ${root} 在 subPackages 字段中不存在`.yellow) 30 | return res 31 | } 32 | 33 | res.push( 34 | config.subPackages[index] 35 | ) 36 | 37 | return res 38 | }, []) 39 | 40 | console.assert(Array.isArray(ignorePages), 'entry.ignore.pages 只接受要忽略的 page 数组') 41 | 42 | config.pages = config.pages.filter(page => ignorePages.indexOf(page) === -1) 43 | 44 | const allowSubPackages = independentPkgs 45 | 46 | config.subPackages.forEach(({ root, pages }) => { 47 | let pkg = { root, pages: [] } 48 | pages.forEach(page => { 49 | if (ignorePages.indexOf(join(root, page)) === -1) { 50 | pkg.pages.push(page) 51 | } 52 | }) 53 | 54 | allowSubPackages.push(pkg) 55 | }) 56 | 57 | config.subPackages = allowSubPackages 58 | } 59 | 60 | function parseUsingComponents (acceptUsingComponents = [], ignoreUsingComponents = [], config) { 61 | validateProptery(acceptUsingComponents, 'entry.accept.usingComponents') 62 | 63 | console.assert(Array.isArray(ignoreUsingComponents), 'entry.ignore.usingComponents 必须是一个数组') 64 | 65 | const usingComponents = config.usingComponents || {} 66 | 67 | if (acceptUsingComponents === true) acceptUsingComponents = Object.keys(usingComponents) 68 | 69 | const copyUsingComponents = {} 70 | 71 | acceptUsingComponents.forEach(key => { 72 | console.assert(usingComponents[key], `entry.accept.usingComponents[${key}] 在 usingComponents 中不存在`.yellow) 73 | copyUsingComponents[key] = usingComponents[key] 74 | }) 75 | 76 | ignoreUsingComponents.forEach(key => { 77 | delete copyUsingComponents[key] 78 | }) 79 | 80 | config.usingComponents = Object.keys(copyUsingComponents).length > 0 ? copyUsingComponents : undefined 81 | } 82 | 83 | module.exports.getEntryConfig = async function (pluginEntryConfig, appJsonConfig) { 84 | const entryConfig = pluginEntryConfig 85 | if (!entryConfig) return appJsonConfig 86 | 87 | const { accept = {}, ignore = {} } = entryConfig 88 | const config = JSON.parse(JSON.stringify(appJsonConfig)) 89 | 90 | // 只要是设置了当前入口的配置,所有非 accept 里面的字段都将视为 ignore 字段 91 | Object.keys(config).forEach(key => { 92 | if (accept[key]) return // 接受字段 93 | 94 | delete config[key] 95 | }) 96 | 97 | parsePackages(accept.pages, accept.subPackages, ignore.pages, config) 98 | await parseUsingComponents(accept.usingComponents, ignore.usingComponents, config) 99 | 100 | return config 101 | } 102 | 103 | module.exports.getAcceptPackages = function (pluginEntryConfig, appJsonConfig) { 104 | const config = JSON.parse(JSON.stringify(appJsonConfig)) 105 | 106 | if (pluginEntryConfig) { 107 | const { accept = {}, ignore = {} } = pluginEntryConfig 108 | 109 | // 只要是设置了当前入口的配置,所有非 accept 里面的字段都将视为 ignore 字段 110 | Object.keys(config).forEach(key => { 111 | if (accept[key]) return // 接受字段 112 | 113 | delete config[key] 114 | }) 115 | 116 | parsePackages(accept.pages, accept.subPackages, ignore.pages, config) 117 | } 118 | 119 | return [ 120 | { 121 | root: '', 122 | name: '主包页面', 123 | pages: config.pages || [] 124 | }, 125 | ...(config.subPackages || []) 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /src/helpers/resolve-asset-content.js: -------------------------------------------------------------------------------- 1 | const { resolveTargetPath } = require('./resolve-target-path') 2 | const { getFile } = require('./get-files') 3 | 4 | module.exports.resolveAssetContent = function (file, distPath, compilation) { 5 | let { assets, cache } = compilation 6 | 7 | distPath = resolveTargetPath(distPath) 8 | 9 | if (assets[distPath]) return assets[distPath].source().toString() 10 | 11 | for (const key in cache) { 12 | if (cache.hasOwnProperty(key)) { 13 | const module = cache[key] 14 | 15 | if (module.buildInfo && module.buildInfo.assets) { 16 | for (const assetName of Object.keys(module.buildInfo.assets)) { 17 | if (getFile(module.resource) === file) { 18 | return module.buildInfo.assets[assetName].source().toString() 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/resolve-component-path.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require('path') 2 | const { readFileSync } = require('fs') 3 | 4 | async function resolveComponent (resolver, context, component) { 5 | component = component + '.json' 6 | // 获取自定义组件的绝对路径 7 | return await resolver(context, component) 8 | } 9 | 10 | function forEachUsingComponent (usingComponents, fn) { 11 | let ps = [] 12 | 13 | for (const key in usingComponents || {}) { 14 | let element = usingComponents[key] 15 | 16 | ps.push(fn(key, element)) 17 | } 18 | 19 | return ps 20 | } 21 | 22 | function forEachComponentGenerics (componentGenerics, fn) { 23 | let ps = [] 24 | 25 | for (const key in componentGenerics) { 26 | if (typeof componentGenerics[key] === 'object') { 27 | for (const _key in componentGenerics[key]) { 28 | ps.push(fn(_key, key)) 29 | } 30 | } else if (componentGenerics[key]) { 31 | fn(key, key) 32 | } 33 | } 34 | 35 | return ps 36 | } 37 | 38 | async function resolveComponentsPath (resolver, request) { 39 | let content = {} 40 | 41 | try { 42 | content = JSON.parse( 43 | readFileSync(request, { encoding: 'utf8' }) 44 | ) 45 | } catch (error) { 46 | console.log(error) 47 | } 48 | 49 | const context = dirname(request) 50 | const components = new Map() 51 | const { componentGenerics, usingComponents, publicComponents } = content 52 | 53 | if (!usingComponents && !componentGenerics && !publicComponents) return components 54 | 55 | /** 56 | * 自定义组件 57 | */ 58 | let normalPromises = forEachUsingComponent(usingComponents, async (key, item) => { 59 | if (/^plugin:\/\//.test(item) || /^plugin-private:\/\//.test(item)) { 60 | components.set(key, { 61 | request, 62 | origin: item, 63 | absPath: item, 64 | type: 'plugin' 65 | }) 66 | return 67 | } 68 | let component = await resolveComponent(resolver, context, item) 69 | 70 | components.set(key, { 71 | request, 72 | origin: item, 73 | absPath: component, 74 | type: 'normal' 75 | }) 76 | }) 77 | 78 | /** 79 | * 插件组件处理和普通插件处理一样 80 | */ 81 | let pluginPromises = forEachUsingComponent(publicComponents, async (key, item) => { 82 | let component = await resolveComponent(resolver, context, item) 83 | components.set(key, { 84 | request, 85 | origin: item, 86 | absPath: component, 87 | type: 'normal' 88 | }) 89 | }) 90 | 91 | /** 92 | * 抽象组件 93 | */ 94 | let genericesPromises = forEachComponentGenerics(componentGenerics, async (key, element) => { 95 | if (componentGenerics[element] === true) { 96 | return components.set(element, { 97 | request, 98 | origin: '', 99 | absPath: '', 100 | type: 'generics' 101 | }) 102 | } 103 | let relPath = componentGenerics[element].default 104 | let component = await resolveComponent(resolver, context, relPath) 105 | components.set(element, { 106 | request, 107 | origin: relPath, 108 | absPath: component, 109 | type: 'generics' 110 | }) 111 | }) 112 | 113 | await Promise.all([ 114 | ...normalPromises, 115 | ...pluginPromises, 116 | ...genericesPromises 117 | ]) 118 | 119 | return components 120 | } 121 | 122 | module.exports.resolveComponentsFiles = async function (jsons, componentSet, resolver, emptyComponent) { 123 | let nextJsons = [] 124 | for (const json of jsons) { 125 | if (emptyComponent && emptyComponent.test(json)) { 126 | continue // 对于需要处理为空组件的不再加载其子组件 127 | } 128 | 129 | let components = await resolveComponentsPath(resolver, json) 130 | 131 | for (const [key, component] of components) { 132 | componentSet.add({ tag: key, component }) 133 | if (component.type === 'normal' || (component.type === 'generics' && component.absPath)) { 134 | nextJsons.push(component.absPath) 135 | } 136 | } 137 | } 138 | 139 | nextJsons.length && await module.exports.resolveComponentsFiles(nextJsons, componentSet, resolver, emptyComponent) 140 | } 141 | -------------------------------------------------------------------------------- /src/helpers/resolve-dist-path.js: -------------------------------------------------------------------------------- 1 | const { join, dirname } = require('path') 2 | const { relative } = require('../utils') 3 | /** 4 | * 资源目录排序,在计算路径时优先根据子路径计算 5 | * [ 6 | * 'path/to/src/', 7 | * 'path/to/src1/', 8 | * 'path/to/src/dir1', 9 | * 'path/to/src/dir2', 10 | * 'path/to/src/dir1/333', 11 | * 'path/to/src/dir2/333', 12 | * ] 13 | * => 14 | * [ 15 | * 'path/to/src/dir1/333', 16 | * 'path/to/src/dir1', 17 | * 'path/to/src/dir2/333', 18 | * 'path/to/src/dir2', 19 | * 'path/to/src1/', 20 | * 'path/to/src/' 21 | * ] 22 | */ 23 | module.exports.orderSource = function orderSource (resources = []) { 24 | /** 25 | * 项目依赖的目录列表,会根据这些目录计算出最后输出路径 26 | */ 27 | const sourceSet = new Set([...resources]) 28 | const sources = Array.from(sourceSet) 29 | 30 | /** 31 | * { 32 | * path: { 33 | * isEndPoint: false, 34 | * to: { 35 | * isEndPoint: false, 36 | * src: { 37 | * isEndPoint: true, 38 | * }, 39 | * src1: { 40 | * isEndPoint: true, 41 | * }, 42 | * } 43 | * } 44 | * } 45 | */ 46 | const tree = {} 47 | 48 | sources.forEach((source, index) => { 49 | let parent = tree 50 | let splited = source.split('/') 51 | splited.forEach((val, index) => { 52 | if (val === '') val = '/' 53 | 54 | const child = parent[val] || { isEndPoint: index === splited.length - 1 } 55 | parent = parent[val] = child 56 | }) 57 | }) 58 | 59 | function resolvePath (tree, key) { 60 | let keys = Object.keys(tree) 61 | 62 | let paths = [] 63 | 64 | keys.forEach(key => { 65 | if (key === 'isEndPoint') return 66 | 67 | let res = resolvePath(tree[key], key) 68 | if (res.length === 0) { 69 | res = [key] 70 | } else { 71 | res = res.map(item => join(key, item)) 72 | } 73 | paths = paths.concat(res) 74 | }) 75 | 76 | if (tree.isEndPoint) { 77 | paths.push('') 78 | } 79 | 80 | return paths 81 | } 82 | 83 | return resolvePath(tree) 84 | } 85 | 86 | module.exports.resolveDepDistPath = function resolveDepDistPath (originDist, additionDist, depdist) { 87 | const relPath = relative(originDist, depdist) 88 | const depDist = join(dirname(additionDist), relPath) 89 | 90 | return depDist 91 | } 92 | -------------------------------------------------------------------------------- /src/helpers/resolve-target-path.js: -------------------------------------------------------------------------------- 1 | const { extname } = require('path') 2 | const CONFIG = { 3 | ali: { 4 | TWxs (path) { 5 | return path.replace('.wxs', '.sjs') 6 | }, 7 | 8 | TWxml (path) { 9 | return path.replace('.wxml', '.axml') 10 | }, 11 | 12 | TWxss (path) { 13 | return path.replace('.wxss', '.acss') 14 | }, 15 | 16 | TScss (path) { 17 | return path.replace('.scss', '.acss') 18 | }, 19 | 20 | TPcss (path) { 21 | return path.replace('.pcss', '.acss') 22 | }, 23 | 24 | TLess (path) { 25 | return path.replace('.less', '.acss') 26 | } 27 | }, 28 | 29 | wx: { 30 | TScss (path) { 31 | return path.replace('.scss', '.wxss') 32 | }, 33 | 34 | TPcss (path) { 35 | return path.replace('.pcss', '.wxss') 36 | }, 37 | 38 | TLess (path) { 39 | return path.replace('.less', '.wxss') 40 | } 41 | } 42 | } 43 | 44 | module.exports.resolveTargetPath = function (file) { 45 | let target = process.env.TARGET || 'wx' 46 | let TARGET = CONFIG[target] 47 | let ext = extname(file) 48 | 49 | if (!ext) throw new Error(`接受到一个不正常的文件${file}`) 50 | 51 | let method = 'T' + ext.substr(1, 1).toUpperCase() + ext.substr(2) 52 | return method && TARGET[method] ? TARGET[method](file) : file 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/update-code.js: -------------------------------------------------------------------------------- 1 | const { dirname, join } = require('path') 2 | const { ConcatSource } = require('webpack-sources') 3 | const { relative } = require('../utils') 4 | const { parse } = require('@babel/parser') 5 | const traverse = require('@babel/traverse') 6 | const t = require('@babel/types') 7 | const generator = require('@babel/generator') 8 | 9 | module.exports.updateJsCode = function updateJsCode (codeSource, originDist, additionDist) { 10 | // 路径一致就返回,没必要计算 11 | if (originDist === additionDist) return codeSource 12 | 13 | let code = codeSource.source().toString() 14 | const ast = parse(code, { scourceType: 'module' }) 15 | /** 16 | * 原来正则情况会导致匹配到注释中的 require,然后进行替换,导致输出路径可能不对 17 | * 比如模块 A 依赖模块 B,模块 B 中有注释 require('module-b-path'),进行替换 18 | * 时,bundle 中的模块 B 路径变为了相对路径,更复杂时就可能出现替换后的路径和实际 19 | * bundle 中路径不一致 20 | */ 21 | traverse.default(ast, { 22 | CallExpression (path) { 23 | if (t.isIdentifier(path.node.callee, { name: 'require' })) { 24 | if (path.node.arguments.length !== 1) { 25 | console.log( 26 | generator.default(path.parent).code 27 | ) 28 | throw Error('require 表达式错误') 29 | } 30 | const filePath = path.node.arguments[0].value 31 | const fileDist = join(dirname(originDist), filePath) 32 | const relPath = relative(additionDist, fileDist) 33 | 34 | path.node.arguments[0].value = relPath 35 | } 36 | } 37 | }) 38 | 39 | code = generator.default(ast).code 40 | 41 | // 有点危险,为了解决 webpack require 如果已经加载模块就不执行导致报 Component 为定义的问题 42 | code = code.replaceAll(originDist, additionDist) 43 | 44 | return new ConcatSource(code) 45 | } 46 | -------------------------------------------------------------------------------- /src/helpers/wxml-parser.js: -------------------------------------------------------------------------------- 1 | const { DomUtils, parseDOM } = require('htmlparser2') 2 | 3 | module.exports.find = function find (content, test) { 4 | let dom = parseDOM(content, { 5 | recognizeSelfClosing: true, 6 | lowerCaseAttributeNames: false 7 | }) 8 | DomUtils.find(test, dom, true) 9 | return dom 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('colors') 2 | require('console.table') 3 | 4 | const MiniLoader = require('./classes/Loader') 5 | 6 | /* eslint-disable */ 7 | String.prototype.replaceAll = function (str, replacement) { 8 | if (str === replacement) return this.toString(); 9 | 10 | str = str.replace(/\./g, '\\.') 11 | return this.replace(new RegExp(str, 'gm'), replacement) 12 | } 13 | 14 | module.exports = function (content) { 15 | this.cacheable && this.cacheable() 16 | return new MiniLoader(this, content) 17 | } 18 | 19 | module.exports.plugin = require('./plugin/MiniProgramPlugin') 20 | 21 | process.on('unhandledRejection', error => { 22 | console.log('unhandledRejection', error) 23 | }) -------------------------------------------------------------------------------- /src/lib/require.js: -------------------------------------------------------------------------------- 1 | var installedModules = {} 2 | 3 | module.exports = function (entry, modules, isEntry = true) { 4 | var entryModule = null 5 | // The require function 6 | function __webpack_require__ (moduleId) { 7 | // Check if module is in cache 8 | if (moduleId === entry && entryModule) { 9 | throw new Error('入口文件' + moduleId + '被加载两次') 10 | } 11 | 12 | if (moduleId !== entry && installedModules[moduleId]) { 13 | return installedModules[moduleId].exports 14 | } 15 | // Create a new module (and put it into the cache) 16 | var module = installedModules[moduleId] = { 17 | i: moduleId, 18 | l: false, 19 | exports: {} 20 | } 21 | 22 | // Execute the module function 23 | if (moduleId === entry) { 24 | entryModule = module 25 | } 26 | 27 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 28 | 29 | // Flag the module as loaded 30 | module.l = true 31 | 32 | // Return the exports of the module 33 | return module.exports 34 | } 35 | 36 | // expose the modules object (__webpack_modules__) 37 | __webpack_require__.m = modules 38 | 39 | // expose the module cache 40 | __webpack_require__.c = installedModules 41 | 42 | // define getter function for harmony exports 43 | __webpack_require__.d = function (exports, name, getter) { 44 | if (!__webpack_require__.o(exports, name)) { 45 | Object.defineProperty(exports, name, { enumerable: true, get: getter }) 46 | } 47 | } 48 | 49 | // define __esModule on exports 50 | __webpack_require__.r = function (exports) { 51 | if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 52 | Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }) 53 | } 54 | Object.defineProperty(exports, '__esModule', { value: true }) 55 | } 56 | 57 | // create a fake namespace object 58 | // mode & 1: value is a module id, require it 59 | // mode & 2: merge all properties of value into the ns 60 | // mode & 4: return value when already ns object 61 | // mode & 8|1: behave like require 62 | __webpack_require__.t = function (value, mode) { 63 | if (mode & 1) value = __webpack_require__(value) 64 | if (mode & 8) return value 65 | if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value 66 | var ns = Object.create(null) 67 | __webpack_require__.r(ns) 68 | Object.defineProperty(ns, 'default', { enumerable: true, value: value }) 69 | if (mode & 2 && typeof value !== 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key] }.bind(null, key)) 70 | return ns 71 | } 72 | 73 | // getDefaultExport function for compatibility with non-harmony modules 74 | __webpack_require__.n = function (module) { 75 | var getter = module && module.__esModule 76 | ? function getDefault () { return module['default'] } 77 | : function getModuleExports () { return module } 78 | __webpack_require__.d(getter, 'a', getter) 79 | return getter 80 | } 81 | 82 | // Object.prototype.hasOwnProperty.call 83 | __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property) } 84 | 85 | // __webpack_public_path__ 86 | __webpack_require__.p = '' 87 | 88 | // Load entry module and return exports 89 | return __webpack_require__(__webpack_require__.s = entry) 90 | } 91 | -------------------------------------------------------------------------------- /src/platform/wx/get-empty-file-source.js: -------------------------------------------------------------------------------- 1 | const { RawSource } = require('webpack-sources') 2 | 3 | module.exports.getEmptyFileSource = function (fileMeta) { 4 | if (fileMeta.isWxml) { 5 | return new RawSource('') 6 | } 7 | 8 | if (fileMeta.isJs) { 9 | return new RawSource('Component({})') 10 | } 11 | 12 | if (fileMeta.isJson) { 13 | return new RawSource('{ "component": true }') 14 | } 15 | 16 | return new RawSource('') 17 | } 18 | -------------------------------------------------------------------------------- /src/platform/wx/wxml.js: -------------------------------------------------------------------------------- 1 | const { resolveAssetContent } = require('../../helpers/resolve-asset-content') 2 | const { ConcatSource } = require('webpack-sources') 3 | 4 | module.exports.loadWxmlContent = function loadContent (compilation, getFileHelper, file, loaded = {}) { 5 | let { deps: depSet, dist } = getFileHelper(file) 6 | let content = resolveAssetContent(file, dist, compilation) 7 | let buff = new ConcatSource() 8 | 9 | for (let { source, isWxs } of depSet) { 10 | // 依赖的文件已经添加不需要再次添加 11 | if (loaded[source]) continue 12 | loaded[source] = true 13 | 14 | if (isWxs) continue 15 | 16 | let depContent = loadContent(compilation, getFileHelper, source, loaded) 17 | 18 | buff.add(depContent) 19 | } 20 | 21 | buff.add(content) 22 | 23 | return buff 24 | } 25 | 26 | const NATIVE_TAGS = new Set([ 27 | 'view', 28 | 'scroll-view', 29 | 'swiper', 30 | 'movable-view', 31 | 'movable-aera', 32 | 'cover-view', 33 | 'cover-image', 34 | 'icon', 35 | 'text', 36 | 'rich-text', 37 | 'progress', 38 | 'button', 39 | 'checkbox', 40 | 'checkbox-group', 41 | 'form', 42 | 'input', 43 | 'label', 44 | 'picker', 45 | 'picker-view', 46 | 'picker-view-column', 47 | 'swiper-item', 48 | 'radio', 49 | 'radio-group', 50 | 'slider', 51 | 'switch', 52 | 'textarea', 53 | 'navigator', 54 | 'functional-page-navigator', 55 | 'audio', 56 | 'image', 57 | 'video', 58 | 'camera', 59 | 'live-player', 60 | 'live-pusher', 61 | 'map', 62 | 'canvas', 63 | 'open-data', 64 | 'web-view', 65 | 'ad', 66 | 'official-account', 67 | 'template', 68 | 'wxs', 69 | 'import', 70 | 'include', 71 | 'block', 72 | 'slot', 73 | 'movable-area', 74 | 'page-meta', 75 | 'editor' 76 | ]) 77 | 78 | module.exports.isNativeTag = function isNativeTag (tag) { 79 | return NATIVE_TAGS.has(tag) 80 | } 81 | -------------------------------------------------------------------------------- /src/plugin/FileEntryPlugin.js: -------------------------------------------------------------------------------- 1 | const target = process.env.TARGET || 'wx' 2 | const { existsSync, readFileSync } = require('fs') 3 | const { Tapable, SyncHook, SyncWaterfallHook } = require('tapable') 4 | const { ConcatSource } = require('webpack-sources') 5 | const { dirname, join, extname, basename } = require('path') 6 | const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin') 7 | const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin') 8 | 9 | const { flattenDeep, isEmpty, getExportFilePath, getExtPath } = require('../utils') 10 | const { getFiles, getFile } = require('../helpers/get-files') 11 | const { mergeEntrys } = require('../helpers/merge-entry') 12 | const { getAcceptPackages } = require('../helpers/parse-entry') 13 | const { createResolver } = require('../helpers/create-resolver') 14 | const { resolveComponentsFiles } = require('../helpers/resolve-component-path') 15 | const { ENTRY_ACCEPT_FILE_EXTS } = require('../config/constant') 16 | const { getEmptyFileSource } = require(`../platform/${target}/get-empty-file-source`) 17 | 18 | const mainChunkNameTemplate = '__assets_chunk_name__' 19 | let mainChunkNameIndex = 0 20 | 21 | const IS_WIN32 = process.platform === 'win32' 22 | 23 | function replacePathSep (ppath) { 24 | return IS_WIN32 ? ppath.replace(/\\/g, '/') : ppath 25 | } 26 | 27 | module.exports = class FileEntryPlugin extends Tapable { 28 | constructor (miniLoader, options) { 29 | super() 30 | this.miniLoader = miniLoader 31 | this._row = {} 32 | this.packages = {} 33 | this._usedValue = {} 34 | this._entrysConfig = options.entry || {} 35 | this._mode = options.mode 36 | this.options = options 37 | this.mainEntry = null 38 | this.chunkNames = [] 39 | 40 | this.hooks = { 41 | addPage: new SyncHook(['page', 'root']), 42 | addFiles: new SyncHook(['files']), 43 | getAppJsonCode: new SyncWaterfallHook(['code']), 44 | getAppXssExt: new SyncHook([]) 45 | } 46 | } 47 | 48 | apply (compiler) { 49 | this.compiler = compiler 50 | this.startTime = Date.now() 51 | this.context = this.compiler.context 52 | this.entrys = this.miniLoader.miniEntrys 53 | this.resolver = createResolver(compiler) 54 | 55 | compiler.hooks.beforeCompile.tapAsync('FileEntryPlugin', this.beforeCompile.bind(this)) 56 | compiler.hooks.emit.tapAsync('FileEntryPlugin', this.setEmitHook.bind(this)) 57 | compiler.hooks.compilation.tap('FileEntryPlugin', this.setCompilation.bind(this)) 58 | 59 | this.isFirstCompile = true 60 | 61 | // 根据已有的入口文件准备好最基础的 app.json 内容,并添加这些文件到 webpack 打包程序 62 | this.start() 63 | // 支持 app.json plugins 中 export 字段 64 | this.setExportFile() 65 | // 添加一些项目相关的额外文件到 webpack 打包程序 66 | this.loadProjectFiles() 67 | } 68 | 69 | setExportFile () { 70 | let appCode 71 | const extPath = getExtPath(this.options.extfile, this.context) 72 | 73 | if (!extPath) { 74 | // 没有 ext.json 时不进行后续逻辑 75 | return 76 | } 77 | 78 | try { 79 | appCode = JSON.parse(readFileSync(extPath, { encoding: 'utf-8' })) 80 | } catch (error) { 81 | console.error('FileEntryPlugin: ', '读取 ext.json 失败') 82 | throw new Error(error) 83 | } 84 | 85 | const filePaths = getExportFilePath(appCode, this.context) 86 | 87 | if (filePaths.length) { 88 | this.miniLoader.fileTree.setFile(filePaths) 89 | this.addScriptEntry(filePaths) 90 | } 91 | } 92 | 93 | setCompilation (compilation) { 94 | this.compilation = compilation 95 | compilation.hooks.optimizeTree.tapAsync('FileEntryPlugin', (chunks, modules, cb) => this.optimizeTree(chunks, modules, cb)) 96 | // 检查是否需要重新编译 97 | compilation.hooks.needAdditionalPass.tap('FileEntryPlugin', () => this.needAdditionalPass ? true : undefined) 98 | } 99 | 100 | /** 101 | * 添加项目依赖的自定义组件到编译 102 | * @param {*} prarams 103 | * @param {*} callback 104 | */ 105 | beforeCompile (prarams, callback) { 106 | // 不是第一次处理的时候不需要进入这个逻辑 107 | if (!this.isFirstCompile) return callback() 108 | 109 | const jsons = [...this.entrys] 110 | Object.keys(this.packages).forEach((root) => { 111 | const { pages } = this.packages[root] 112 | 113 | pages.forEach(page => { 114 | const pageJson = `${page}.json` 115 | 116 | if (existsSync(pageJson)) { 117 | jsons.push(pageJson) 118 | } 119 | }) 120 | }) 121 | 122 | /** 123 | * 添加依赖的自定义组件 124 | */ 125 | this.loadComponentsFiles(jsons) 126 | .then(() => callback()) 127 | } 128 | /** 129 | * 处理自定义组件文件依赖 130 | * @param {*} chunks 131 | * @param {*} modules 132 | * @param {*} callback 133 | */ 134 | optimizeTree (chunks, modules, callback) { 135 | if (this.isFirstCompile) { 136 | this.isFirstCompile = false 137 | return callback() 138 | } 139 | 140 | const { lastTimestamps = new Map() } = this 141 | const timestamps = this.compilation.fileTimestamps 142 | 143 | const jsons = [] 144 | let files = [] 145 | /** 146 | * 获取所有 json 文件中对自定义组件的依赖 147 | */ 148 | modules.forEach(module => { 149 | const jsonPath = module.resource && getFile(module.resource) 150 | 151 | if (jsonPath && /\.json/.test(jsonPath)) { 152 | if ((lastTimestamps.get(jsonPath) || this.startTime) < (timestamps.get(jsonPath) || Infinity)) { 153 | // 如果是入口文件改变,因为涉及到最后计算输出 json 内容需要更新 154 | // 新页面需要添加到编译 155 | if (this._row[jsonPath]) { 156 | const newPageFiles = this.start() 157 | 158 | files = files.concat(newPageFiles) 159 | } 160 | 161 | jsons.push(getFile(jsonPath)) 162 | } 163 | } 164 | }) 165 | 166 | if (!jsons.length) return callback() 167 | 168 | this.loadComponentsFiles(jsons) 169 | .then((comFiles) => { 170 | files = files.concat(comFiles) 171 | this.needAdditionalPass = files.length > 0 172 | this.hooks.addFiles.call(files) 173 | 174 | callback() 175 | }) 176 | } 177 | 178 | setEmitHook (compilation, callback) { 179 | let assets = compilation.assets 180 | 181 | assets['app.js'] = this.getAppJsCode(assets) 182 | if (!assets['app.js']) { 183 | delete assets['app.js'] 184 | assets['app.js'] = new ConcatSource('App({});') 185 | this.compilation.warnings.push('没有对应的 app.js 文件') 186 | } 187 | 188 | assets['app.wxss'] = this.getAppWxss(assets) 189 | if (!assets['app.wxss'] || !assets['app.wxss'].source()) { 190 | delete assets['app.wxss'] 191 | // this.compilation.warnings.push('没有对应的 app.wxss 文件') 192 | } 193 | 194 | assets['app.json'] = this.getAppJsonCode() 195 | 196 | if (this.options.extfile) { 197 | assets['ext.json'] = this.getExtJson(assets) 198 | } 199 | 200 | const { emptyComponent } = this.options 201 | if (emptyComponent) { 202 | Object.keys(assets).forEach(file => { 203 | if (emptyComponent.test(file)) { 204 | const fileMeta = this.miniLoader.fileTree.getFileByDist(file) 205 | 206 | assets[file] = getEmptyFileSource(fileMeta) 207 | } 208 | }) 209 | } 210 | 211 | this.removeIgnoreAssets(assets) 212 | 213 | callback() 214 | } 215 | 216 | getEntryName (entry) { 217 | let dist = this.miniLoader.outputUtil.get(entry) 218 | 219 | return basename(dist, '.json') 220 | } 221 | 222 | getAppJsCode (assets) { 223 | const mainEntryName = this.getEntryName(this.mainEntry) 224 | return assets[`${mainEntryName}.js`] 225 | } 226 | 227 | getAppWxss (assets) { 228 | let source = new ConcatSource() 229 | 230 | this.entrys.forEach(entry => { 231 | let entryName = this.getEntryName(entry) 232 | if (!assets[`${entryName}.wxss`]) return 233 | 234 | source.add(`/* ${entryName}.wxss */\n`) 235 | source.add( 236 | assets[`${entryName}.wxss`] 237 | ) 238 | }) 239 | 240 | return source 241 | } 242 | 243 | getAppJson () { 244 | let appCode = mergeEntrys(this._row) 245 | 246 | // 需要考虑到 this.packages 包括了主包(主包单独作为一个特殊分包进入编译流程),所以 if 需要 +1 247 | // 如果有过滤条件的,那么直接删除 preloadRule 248 | if (appCode.subPackages.length > Object.keys(this.packages).length + 1) { 249 | delete appCode.preloadRule 250 | } 251 | 252 | delete appCode.pages 253 | delete appCode.subPackages 254 | delete appCode.usingComponents 255 | 256 | if (Object.keys(this._entrysConfig).length) { 257 | delete appCode.preloadRule 258 | delete appCode.tabBar 259 | } 260 | 261 | appCode.subPackages = [] 262 | 263 | for (const root in this.packages) { 264 | if (this.packages.hasOwnProperty(root)) { 265 | const element = this.packages[root] 266 | 267 | if (!element.root) { 268 | appCode.pages = (element.pages || []).map((page) => replacePathSep(this.miniLoader.outputUtil.get(page))) 269 | } else { 270 | const { pages: subPages, root } = element 271 | 272 | const distPages = subPages.map((page) => { 273 | const dist = replacePathSep(this.miniLoader.outputUtil.get(page)) 274 | 275 | return dist.replace(`${root}/`, '') 276 | }) 277 | 278 | appCode.subPackages.push({ 279 | ...element, 280 | pages: distPages 281 | }) 282 | } 283 | } 284 | } 285 | 286 | if (appCode.pages.length === 0 && appCode.subPackages.length !== 0) { 287 | let mainPkg = appCode.subPackages.pop() 288 | 289 | appCode.pages = mainPkg.pages.map(page => { 290 | return `${mainPkg.root}/${page}` 291 | }) 292 | } 293 | 294 | return appCode 295 | } 296 | 297 | getAppJsonCode () { 298 | const code = this.hooks.getAppJsonCode.call(this.getAppJson()) 299 | 300 | return new ConcatSource(JSON.stringify(code)) 301 | } 302 | 303 | getExtJson (assets) { 304 | if (typeof this.options.extfile === 'string') { 305 | const dist = this.miniLoader.outputUtil.get(this.options.extfile) 306 | return assets[dist] 307 | } 308 | 309 | return assets['ext.json'] 310 | } 311 | 312 | get ignoreFiles () { 313 | let files = this.chunkNames.map(fileName => `${fileName}.js`) 314 | 315 | this.entrys.forEach(entry => { 316 | let entryName = this.getEntryName(entry) 317 | 318 | if (entryName !== 'app') { 319 | files.push( 320 | ...ENTRY_ACCEPT_FILE_EXTS.map(ext => `${entryName}${ext}`) 321 | ) 322 | } 323 | }) 324 | 325 | return files 326 | } 327 | 328 | /** 329 | * 删除不需要输出的文件 330 | * @param {*} assets 331 | */ 332 | removeIgnoreAssets (assets) { 333 | this.ignoreFiles.forEach(file => { 334 | delete assets[file] 335 | }) 336 | } 337 | 338 | start (params, callback) { 339 | this.entrys.forEach(entry => { 340 | this._row[entry] && this.clearEntryPackages(entry) 341 | const pkgs = this.getEntryPackages(entry) 342 | this.mergePackges(entry, pkgs) 343 | }) 344 | 345 | const files = [] 346 | 347 | Object.keys(this.packages).forEach((root) => { 348 | const { pages, independent, entry } = this.packages[root] 349 | pages.forEach(page => { 350 | // TODO 可以更改页面的路径 351 | const pageFiles = getFiles('', page) 352 | 353 | pageFiles.forEach(file => !this.miniLoader.fileTree.has(file) && files.push(file)) 354 | 355 | this.miniLoader.fileTree.addPage(page, pageFiles, !!root, independent, entry) 356 | }) 357 | }) 358 | 359 | this.addEntrys(files) 360 | 361 | return files 362 | } 363 | 364 | loadProjectFiles () { 365 | let files = [] 366 | let extfile = this.options.extfile 367 | 368 | const context = dirname(this.mainEntry) 369 | if (extfile) { 370 | files.push( 371 | extfile === true 372 | ? getFiles(context, 'ext', ['.json']) 373 | : extfile 374 | ) 375 | } 376 | 377 | files.push( 378 | getFiles(context, 'project.config', ['.json']) 379 | ) 380 | 381 | if (!this.options.ignoreTabbar) { 382 | // 对于不同平台需要单独处理 383 | let tabBar = this._row[this.mainEntry].tabBar 384 | 385 | !tabBar && Object.keys(this._row).forEach(entry => { 386 | const code = this._row[entry] 387 | if (entry !== this.mainEntry) { 388 | tabBar = code.tabBar 389 | } 390 | }) 391 | 392 | tabBar && tabBar.list && tabBar.list.forEach( 393 | ({ selectedIconPath, iconPath }) => { 394 | selectedIconPath && files.push( 395 | join(context, selectedIconPath) 396 | ) 397 | iconPath && files.push(join(context, iconPath)) 398 | } 399 | ) 400 | } 401 | 402 | this.addEntrys(files) 403 | this.miniLoader.fileTree.setFile( 404 | flattenDeep(files) 405 | ) 406 | } 407 | 408 | getEntryPackages (entry) { 409 | const dir = dirname(entry) 410 | const fileName = basename(entry, '.json') 411 | const exts = ['.wxss', '.scss', '.less'] 412 | 413 | if (!this.mainEntry) { 414 | this.mainEntry = entry 415 | exts.push('.js', '.ts') 416 | } 417 | const files = getFiles(dir, fileName, exts) 418 | 419 | this.addEntrys(files, true) 420 | this.miniLoader.fileTree.addEntry(entry, files) 421 | 422 | this._row[entry] = JSON.parse( 423 | readFileSync(entry, { encoding: 'utf8' }) 424 | ) 425 | 426 | return getAcceptPackages(this._entrysConfig[entry], this._row[entry]) 427 | } 428 | 429 | mergePackges (entry, newPackages) { 430 | const context = dirname(entry) 431 | const pkgs = this.packages 432 | newPackages.forEach(({ root, pages, name, independent, plugins }) => { 433 | const pkg = pkgs[root] = pkgs[root] || { } 434 | 435 | pages = pages.map(page => join(context, root, page)) 436 | if (!isEmpty(pkg)) { 437 | console.assert(Boolean(pkg.independent) === Boolean(independent), `独立分包不支持与非独立分包合并: ${root}`) 438 | 439 | pkgs[root].pages = [ 440 | ...new Set([ 441 | ...pkgs[root].pages, 442 | ...pages 443 | ]) 444 | ] 445 | return 446 | } 447 | 448 | pkgs[root] = { 449 | pages, 450 | name, 451 | root, 452 | context, 453 | entry, 454 | independent, 455 | plugins 456 | } 457 | }) 458 | } 459 | 460 | clearEntryPackages (entry) { 461 | const context = dirname(entry) 462 | const { pages = [], subPackages = [] } = this._row[entry] || {} 463 | const pkgs = this.packages 464 | 465 | ;[ 466 | { 467 | root: '', 468 | pages 469 | }, 470 | ...subPackages 471 | ].forEach(({ root, pages }) => { 472 | let cachePages = pkgs[root].pages 473 | 474 | pages.forEach(page => { 475 | let index = cachePages.indexOf(join(context, root, page)) 476 | 477 | if (index !== -1) { 478 | cachePages.splice(index, 1) 479 | } 480 | }) 481 | }) 482 | } 483 | 484 | loadComponentsFiles (jsons) { 485 | const componentSet = new Set() 486 | const files = [] 487 | /** 488 | * 第一次加载时,一次性把所以文件都添加到编译,减少编译时间 489 | */ 490 | return resolveComponentsFiles(jsons, componentSet, this.resolver, this.options.emptyComponent) 491 | .then(() => { 492 | const jsons = Array.from(componentSet) 493 | jsons.forEach(({ tag, component }) => { 494 | files.push( 495 | ...this.addConponent(tag, component) 496 | ) 497 | }) 498 | 499 | this.addEntrys(files) 500 | 501 | return files 502 | }) 503 | } 504 | 505 | addConponent (tag, item) { 506 | // TODO 开口子接受替换自定义组件的路径 507 | 508 | // 自定义组件依赖文件收集 509 | const component = item.absPath 510 | const context = dirname(component) 511 | const path = basename(component, '.json') 512 | let componentfiles = [] 513 | 514 | if (item.type === 'normal' || (item.type === 'generics' && component)) { 515 | componentfiles = getFiles(context, path) 516 | } 517 | 518 | const newFiles = componentfiles.filter( 519 | file => !this.miniLoader.fileTree.has(getFile(file)) 520 | ) 521 | 522 | // 添加文件关系 523 | this.miniLoader.fileTree.addComponent(item.request, tag, component, componentfiles, item.type) 524 | 525 | return newFiles 526 | } 527 | 528 | addEntrys (files, isApp) { 529 | let assetFiles = [] 530 | let scriptFiles = [] 531 | 532 | files = flattenDeep(files) 533 | 534 | files.forEach(file => { 535 | file = file + `?isEntry=true&from=miniapp${isApp ? '&isApp' : ''}` 536 | 537 | return /\.[j|t]s$/.test(getFile(file)) ? scriptFiles.push(file) : assetFiles.push(file) 538 | }) 539 | 540 | this.addAssetsEntry(assetFiles) 541 | this.addScriptEntry(scriptFiles) 542 | } 543 | 544 | addAssetsEntry (entrys) { 545 | let chunkName = mainChunkNameTemplate + mainChunkNameIndex 546 | this.chunkNames.push(chunkName) 547 | new MultiEntryPlugin(this.context, entrys, chunkName).apply(this.compiler) 548 | 549 | // 自动生成 550 | mainChunkNameIndex++ 551 | } 552 | 553 | addScriptEntry (entrys) { 554 | for (const entry of entrys) { 555 | const file = getFile(entry) 556 | let fileName = this.miniLoader.outputUtil.get(file).replace(extname(file), '') 557 | new SingleEntryPlugin(this.context, entry, fileName).apply(this.compiler) 558 | } 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/plugin/MiniProgramPlugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const readline = require('readline') 3 | const { Tapable, SyncHook } = require('tapable') 4 | const { dirname, basename, join } = require('path') 5 | const { ProgressPlugin } = require('webpack') 6 | const { ConcatSource } = require('webpack-sources') 7 | 8 | const { relative, removeExt } = require('../utils') 9 | 10 | const Wxml = require('../classes/Wxml') 11 | const Loader = require('../classes/Loader') 12 | const FileTree = require('../classes/FileTree') 13 | const OutPutPath = require('../classes/OutPutPath') 14 | const ModuleHelper = require('../classes/ModuleHelper') 15 | 16 | const FileEntryPlugin = require('./FileEntryPlugin') 17 | const MiniTemplatePlugin = require('./MiniTemplatePlugin') 18 | 19 | const { normalEntry } = require('../helpers/normal-entrys') 20 | const { calcCodeDep } = require('../helpers/calc-code-dep') 21 | const { analyzeGraph } = require('../helpers/analyze-graph') 22 | const { getContentHash } = require('../helpers/calc-content-hash') 23 | 24 | const defaultOptions = { 25 | extfile: true, 26 | // commonSubPackages: true, 27 | analyze: false, 28 | silently: false, 29 | resources: [], 30 | useFinalCallback: false, 31 | compilationFinish: null, 32 | // forPlugin: false, 33 | ignoreTabbar: false, 34 | optimizeIgnoreDirs: [], 35 | optimizeMainPackage: true, 36 | setSubPackageCacheGroup: null, 37 | computedAssetsFinish: null, 38 | entry: { 39 | // 入口文件的配置 40 | // ignore 41 | // accept 42 | } 43 | } 44 | 45 | const stdout = process.stdout 46 | 47 | module.exports = class MiniProgramPlugin extends Tapable { 48 | constructor (options) { 49 | super() 50 | this.undefinedTagTable = new Map() 51 | this.definedNotUsedTable = new Map() 52 | this.unDeclareComponentTable = new Map() 53 | this.options = Object.assign( 54 | defaultOptions, 55 | options 56 | ) 57 | 58 | this.buildId = 0 59 | 60 | this.fileTree = new FileTree(this) 61 | 62 | this.startTime = Date.now() 63 | 64 | this.hooks = { 65 | apply: new SyncHook(['self']) 66 | } 67 | 68 | Loader.$applyPluginInstance(this) 69 | } 70 | 71 | apply (compiler) { 72 | this.compiler = compiler 73 | this.compiler.miniLoader = this 74 | this.outputPath = compiler.options.output.path 75 | 76 | const rawEntrys = compiler.options.entry 77 | let { normalEntrys, miniEntrys } = normalEntry(compiler.context, rawEntrys) 78 | 79 | this.miniEntrys = miniEntrys 80 | 81 | const entryDirs = this.miniEntrys.map(entry => dirname(entry)) 82 | 83 | // 设置计算打包后路径需要的参数(在很多地方需要使用) 84 | this.outputUtil = new OutPutPath( 85 | compiler.context, 86 | [ 87 | ...entryDirs, 88 | ...this.options.resources 89 | ], 90 | this.outputPath 91 | ) 92 | 93 | this.FileEntryPlugin = new FileEntryPlugin(this, this.options) 94 | this.FileEntryPlugin.apply(compiler) 95 | 96 | this.moduleHelper = new ModuleHelper(this) 97 | 98 | new MiniTemplatePlugin(this).apply(compiler) 99 | !this.options.silently && new ProgressPlugin({ handler: this.progress }).apply(compiler) 100 | 101 | compiler.hooks.environment.tap('MiniProgramPlugin', this.setEnvHook.bind(this)) 102 | compiler.hooks.compilation.tap('MiniProgramPlugin', this.setCompilation.bind(this)) 103 | compiler.hooks.beforeCompile.tap('MiniProgramPlugin', this.beforeCompile.bind(this)) 104 | compiler.hooks.emit.tapAsync('MiniProgramPlugin', this.setEmitHook.bind(this)) 105 | 106 | if (!normalEntrys) { 107 | normalEntrys = { main: miniEntrys } 108 | this.FileEntryPlugin.chunkNames.push('main') 109 | } 110 | 111 | compiler.options.entry = normalEntrys 112 | } 113 | 114 | setEnvHook () { 115 | let watch = this.compiler.watch 116 | let run = this.compiler.run 117 | // 下面两个地方都在使用 thisMessageOutPut, 先存起来 118 | const thisMessageOutPut = finalCallback => { 119 | const that = this 120 | 121 | return function () { 122 | return that.options.useFinalCallback && typeof finalCallback === 'function' 123 | ? finalCallback.apply(null, arguments) 124 | : that.messageOutPut.apply(that, arguments) 125 | } 126 | } 127 | 128 | this.compiler.watch = (options, finalCallback) => watch.call( 129 | this.compiler, 130 | this.compiler.options, 131 | thisMessageOutPut(finalCallback) 132 | ) 133 | 134 | this.compiler.run = (finalCallback) => { 135 | return run.call(this.compiler, thisMessageOutPut(finalCallback)) 136 | } 137 | } 138 | 139 | /** 140 | * 根据 app.json 设置 cacheGroup 141 | */ 142 | beforeCompile () { 143 | this.undefinedTagTable.clear() 144 | this.definedNotUsedTable.clear() 145 | this.unDeclareComponentTable.clear() 146 | 147 | let appJson = this.FileEntryPlugin.getAppJson() 148 | let cachegroups = this.compiler.options.optimization.splitChunks.cacheGroups 149 | 150 | if (this.options.setSubPackageCacheGroup) { 151 | let groups = this.options.setSubPackageCacheGroup(this, appJson) 152 | Object.assign(cachegroups, groups) 153 | } 154 | } 155 | 156 | setCompilation (compilation) { 157 | this.compilation = compilation 158 | 159 | // 统一输出路径 160 | compilation.hooks.optimizeChunks.tap('MiniProgramPlugin', this.optimizeChunks.bind(this, compilation)) 161 | compilation.mainTemplate.hooks.assetPath.tap('MiniProgramPlugin', path => this.outputUtil.get(path)) 162 | // 添加额外文件 163 | compilation.hooks.additionalAssets.tapAsync('MiniProgramPlugin', callback => this.additionalAssets(compilation, callback)) 164 | } 165 | 166 | /** 167 | * 输出打包进度 168 | * @param {String} progress 进度 169 | * @param {String} event 170 | * @param {*} modules 171 | */ 172 | progress (progress, event, modules) { 173 | readline.clearLine(process.stdout) 174 | readline.cursorTo(process.stdout, 0) 175 | 176 | if (+progress === 1) return 177 | stdout.write(`${'正在打包: '.gray} ${`${(progress * 100).toFixed(2)}%`.green} ${event || ''} ${modules || ''}`) 178 | } 179 | 180 | /** 181 | * 记录 js 文件被使用次数,不添加到 fileTree 是因为添加后会导致计算 deps 复杂 182 | * @param {*} chunks 183 | */ 184 | optimizeChunks (compilation, chunks) { 185 | const ignoreChunkNames = this.FileEntryPlugin.chunkNames 186 | const fileUsedTemp = {} 187 | 188 | for (const chunk of chunks) { 189 | // 记录模块之间依赖关系 190 | if (chunk.hasEntryModule() && ignoreChunkNames.indexOf(`${chunk.name}.js`) === -1) { 191 | for (const module of chunk.getModules()) { 192 | if (!module.isEntryModule()) { 193 | const resourcePath = module.resource 194 | if (!resourcePath) { 195 | compilation.warnings.push( 196 | new Error('请不要动态 require 一个模块,或者使用内置模块') 197 | ) 198 | continue 199 | } 200 | let chunkName = chunk.name + '.js' 201 | 202 | const fileUsed = fileUsedTemp[resourcePath] = fileUsedTemp[resourcePath] || new Set() 203 | 204 | fileUsed.add(chunkName) 205 | 206 | module._usedModules = fileUsed 207 | 208 | this.moduleHelper.addDep(chunkName, this.outputUtil.get(resourcePath)) 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | hasChange (fileMeta, source) { 216 | const contentHash = getContentHash(this.compilation, source) 217 | if (fileMeta.hash === contentHash) { 218 | return false 219 | } 220 | 221 | fileMeta.updateHash(contentHash) 222 | 223 | return true 224 | } 225 | /** 226 | * 获取 wxml 文件中使用到的自定义组件 227 | * @param {*} compilation 228 | * @param {*} callback 229 | */ 230 | setEmitHook (chunks, callback) { 231 | const { compilation } = this 232 | const { assets, fileTimestamps } = compilation 233 | const cantBeRemoveJsonFiles = [] 234 | const cacheRemovedJsonFiles = {} 235 | const getJsonFile = (file) => join( 236 | dirname(file), 237 | `${basename(file, '.wxml')}.json` 238 | ) 239 | 240 | const changeFiles = [] 241 | Object.keys(assets).forEach(file => { 242 | const fileMeta = this.fileTree.getFileByDist(file) 243 | 244 | const hasChange = this.hasChange(fileMeta, assets[file]) 245 | 246 | if (!hasChange) { 247 | // 删除的自定义组件 json 文件先暂存,在 wxml 改变后需要恢复 248 | fileMeta.isJson && 249 | (fileMeta.isComponentFile || fileMeta.isPageFile) && 250 | (cacheRemovedJsonFiles[file] = assets[file]) 251 | 252 | cantBeRemoveJsonFiles.indexOf(file) === -1 && delete assets[file] 253 | return 254 | } 255 | 256 | // wxml 文件改变有可能会引起 json 文件中自定义组件的重新计算 257 | if (fileMeta.isWxml && (fileMeta.isComponentFile || fileMeta.isPageFile)) { 258 | const jsonFile = getJsonFile(file) 259 | // 首先去要确认 json 文件没有在之前被删除,删除了需要恢复 260 | if (cacheRemovedJsonFiles[jsonFile]) { 261 | assets[jsonFile] = cacheRemovedJsonFiles[jsonFile] 262 | changeFiles.push(jsonFile) 263 | } 264 | 265 | cantBeRemoveJsonFiles.push(jsonFile) 266 | } 267 | 268 | // 没有修改的文件直接不输出,减少计算 269 | hasChange && changeFiles.push(file) 270 | }) 271 | 272 | changeFiles.forEach(file => { 273 | const { isTemplate, isWxml, source: filePath } = this.fileTree.getFileByDist(file) 274 | 275 | /** 276 | * 收集 wxml 文件中使用到的自定义组件 277 | */ 278 | if (!isTemplate && isWxml) { 279 | const wxml = new Wxml(this, compilation, filePath, file) 280 | 281 | const usedComponents = wxml.usedComponents() 282 | 283 | const jsonFile = getJsonFile(filePath) 284 | 285 | if (fs.existsSync(jsonFile)) { 286 | this.fileTree.addFullComponent(jsonFile, usedComponents) 287 | } 288 | } 289 | }) 290 | 291 | changeFiles.forEach(dist => { 292 | const meta = this.fileTree.getFileByDist(dist) 293 | const { source: filePath } = meta 294 | 295 | const sourceCode = assets[dist] 296 | // 文件在哪些分包使用 297 | const usedPkgs = this.moduleHelper.onlyUsedInSubPackagesReturnRoots(filePath) 298 | const subRoot = this.moduleHelper.fileIsInSubPackage(filePath) 299 | const ignoreOptimizeFile = this.options.optimizeIgnoreDirs.filter(dirName => dist.match(dirName)).length > 0 300 | /** 301 | * 文件在主包,并且没有被分包文件依赖,直接计算输出 302 | * 否则表示文件只被分包使用,需要移动到分包内 303 | */ 304 | if ((!subRoot && (!usedPkgs || !usedPkgs.length)) || !this.options.optimizeMainPackage || ignoreOptimizeFile) { 305 | /** 306 | * 计算出真实代码 307 | */ 308 | const source = calcCodeDep(this, dist, meta, sourceCode, depFile => { 309 | const distPath = this.outputUtil.get(depFile) 310 | 311 | this.moduleHelper.addDep(dist, distPath) 312 | return relative(dist, distPath) 313 | }) 314 | 315 | // 删除原始的数据,以计算返回的内容为准 316 | assets[dist] = source 317 | 318 | return 319 | } 320 | 321 | /// 文件最后要输出的地址 322 | let outDist = [] 323 | 324 | /** 325 | * 文件在分包 326 | */ 327 | if (subRoot) { 328 | // 文件在分包,而被其他分包引用,则报错 329 | if (usedPkgs && usedPkgs.length && usedPkgs.some(p => p !== subRoot)) { 330 | throw new Error(`文件 ${dist} 在所属分包外引用,决不允许: [${usedPkgs.join(', ')}]`) 331 | } 332 | 333 | // 只需要计算依赖的文件的路径,输出路径不会被改变 334 | outDist.push({ 335 | root: subRoot, 336 | dist 337 | }) 338 | } else { 339 | // 在主包的,并且只被分包使用 340 | outDist = usedPkgs.map((root) => { 341 | let to = join(root, dist) 342 | // 如果输出文件原本就存在 343 | if (assets[to]) { 344 | throw new Error(`分包 ${root} 内存在和 ${dist} 移动后一样的文件,需要修改其中一处后才能构建`) 345 | } 346 | return { 347 | root, 348 | dist: to 349 | } 350 | }) 351 | } 352 | 353 | // 删除源文件的输出 354 | delete assets[dist] 355 | 356 | outDist.forEach(({ dist, root }) => { 357 | /** 358 | * 计算出真实代码 359 | */ 360 | const source = calcCodeDep(this, dist, meta, sourceCode, (depFile) => { 361 | const distPath = this.outputUtil.get(depFile) 362 | const usedPkgs = this.moduleHelper.onlyUsedInSubPackagesReturnRoots(depFile) 363 | const usedInSubPackage = this.moduleHelper.fileIsInSubPackage(depFile) 364 | 365 | const ignoreOptimizeFile = this.options.optimizeIgnoreDirs.filter(dirName => depFile.match(dirName)).length > 0 366 | 367 | // 依赖文件在主包并且被主包依赖,那么最后输出的路径不会变 368 | if ((!usedInSubPackage && !usedPkgs) || ignoreOptimizeFile) { 369 | this.moduleHelper.addDep(dist, distPath) 370 | return relative(dist, distPath) 371 | } 372 | 373 | // 不管文件在主包还是分包,最后都会移动到这个分包 374 | const sub = usedPkgs.filter((subpkgRoot) => subpkgRoot === root) 375 | 376 | // 依赖在分包被使用,那么一定应该存在一个在这个文件所在的分包被使用 377 | if (sub.length === 0) { 378 | throw new Error('...') 379 | } 380 | 381 | const depDist = usedInSubPackage ? distPath : join(root, distPath) 382 | 383 | this.moduleHelper.addDep(dist, distPath) 384 | // 移动依赖文件到子包 385 | return relative(dist, depDist) 386 | }) 387 | 388 | // 新增文件或者替换原文件内容 389 | assets[dist] = source 390 | }) 391 | }) 392 | 393 | this.lastTimestamps = fileTimestamps 394 | 395 | typeof this.options.computedAssetsFinish === 'function' && this.options.computedAssetsFinish.call(null, compilation) 396 | callback() 397 | } 398 | 399 | /** 400 | * 添加模块加载代码 401 | * @param {*} compilation 402 | * @param {*} callback 403 | */ 404 | additionalAssets (compilation, callback) { 405 | compilation.assets['webpack-require.js'] = new ConcatSource( 406 | fs.readFileSync(join(__dirname, '../lib/require.js'), 'utf8') 407 | ) 408 | callback() 409 | } 410 | 411 | pushUndefinedTag (file, tags) { 412 | tags.forEach(tag => { 413 | const tagUsed = this.undefinedTagTable.get(tag) || new Set() 414 | 415 | if (!this.undefinedTagTable.has(tag)) { 416 | this.undefinedTagTable.set(tag, tagUsed) 417 | } 418 | 419 | tagUsed.add( 420 | this.outputUtil.get(file) 421 | ) 422 | }) 423 | } 424 | 425 | pushDefinedNotUsedTag (file, tags) { 426 | tags.forEach(tag => { 427 | const tagUsed = this.definedNotUsedTable.get(tag) || new Set() 428 | 429 | if (!this.definedNotUsedTable.has(tag)) { 430 | this.definedNotUsedTable.set(tag, tagUsed) 431 | } 432 | 433 | tagUsed.add( 434 | this.outputUtil.get(file) 435 | ) 436 | }) 437 | } 438 | 439 | /** 440 | * 添加未申明 component: true 的组件 441 | * @param {string} file 442 | */ 443 | pushUnDeclareComponentTag (file) { 444 | if (this.fileTree.hasPage(removeExt(file))) { 445 | return 446 | } 447 | this.unDeclareComponentTable.set(this.outputUtil.get(file), [file]) 448 | } 449 | 450 | messageOutPut (err, stats) { 451 | const log = (...rest) => (console.log(...rest) || true) 452 | 453 | try { 454 | this.options.compilationFinish && 455 | this.options.compilationFinish(err, stats, this.FileEntryPlugin.getAppJson(), this) 456 | } catch (e) { 457 | stats.compilation.errors.push(e) 458 | } 459 | 460 | if (err) return log(err) 461 | 462 | const { startTime, endTime } = stats 463 | 464 | if (!this.options.silently) { 465 | // @ts-ignore 466 | readline.clearLine(process.stdout) 467 | readline.cursorTo(process.stdout, 0) 468 | stdout.write( 469 | `[${(new Date()).toLocaleTimeString().gray}] [${('id ' + ++this.buildId).gray}] ` + 470 | ((endTime - startTime) / 1000).toFixed(2) + 's ' + 471 | 'Build finish'.green 472 | ) 473 | 474 | const { 475 | warnings = [], 476 | errors = [] 477 | } = stats.compilation 478 | if (warnings.length) { 479 | this.logError(warnings) 480 | } 481 | 482 | if (errors.length) { 483 | this.logError(errors) 484 | } 485 | } 486 | 487 | this.logWarningTable(this.undefinedTagTable, '存在未在 json 文件中定义的组件') 488 | this.logWarningTable(this.definedNotUsedTable, '存在定义后未被使用的组件') 489 | this.logWarningTable(this.unDeclareComponentTable, '存在未申明 component: true 的组件') 490 | 491 | analyzeGraph(stats, this.compilation) 492 | 493 | this.options.analyze && fs.writeFileSync( 494 | join(process.cwd(), 'analyze.json'), 495 | JSON.stringify(this.moduleHelper.toJson(), null, 2) 496 | ) 497 | } 498 | 499 | logWarningTable (table, title) { 500 | const log = (...rest) => (console.log(...rest) || true) 501 | 502 | if (table.size) { 503 | log('\n') 504 | log(title.red) 505 | 506 | table.forEach((files, tag) => { 507 | log(`[${tag.yellow}]`) 508 | 509 | files.forEach(val => log(' ', val.gray)) 510 | }) 511 | } 512 | } 513 | 514 | logError (messages) { 515 | messages.forEach((err) => { 516 | if (!err.module || !err.module.id) { 517 | return console.log(err) 518 | } 519 | 520 | let message = err.message.split(/\n\n|\n/) 521 | let mainMessage = message[0] || '' 522 | let lc = mainMessage.match(/\((\d+:\d+)\)/) 523 | lc = lc ? lc[1] : '1:1' 524 | 525 | console.log('Error in file', (err.module && err.module.id + ':' + lc).red) 526 | console.log(mainMessage.gray) 527 | message[1] && console.log(message[1].gray) 528 | message[2] && console.log(message[2].gray) 529 | console.log('') 530 | }) 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/plugin/MiniTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { readFileSync } = require('fs') 3 | const { ConcatSource, RawSource } = require('webpack-sources') 4 | const requireCode = require('../lib/require') 5 | const utils = require('../utils') 6 | const { getFile } = require('../helpers/get-files') 7 | const qs = require('querystring') 8 | 9 | module.exports = class MiniTemplate { 10 | constructor (miniLoader) { 11 | this.miniLoader = miniLoader 12 | this.outPath = this.miniLoader.outputPath 13 | this.requirePath = join(this.outPath, './webpack-require') 14 | } 15 | 16 | apply (compiler) { 17 | this.compiler = compiler 18 | this.targetIsUMD = compiler.options.output.libraryTarget === 'umd' 19 | const extPath = utils.getExtPath(this.miniLoader.options.extfile, compiler.context) 20 | 21 | this.extCode = extPath ? JSON.parse(readFileSync(extPath, { encoding: 'utf-8' })) : {} 22 | 23 | compiler.hooks.compilation.tap('MiniTemplate', (compilation) => { 24 | this.compilation = compilation 25 | 26 | compilation.mainTemplate.hooks.render.tap('MiniTemplate', this.setRender.bind(this)) 27 | }) 28 | } 29 | 30 | getRequirePath (entry) { 31 | const [filePath, querystr] = entry.split('?') 32 | const query = qs.parse(querystr) 33 | 34 | const entryPath = join( 35 | this.outPath, 36 | query.output || this.miniLoader.outputUtil.get(filePath) 37 | ) 38 | 39 | return utils.relative(entryPath, this.requirePath) 40 | } 41 | 42 | setRender (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) { 43 | try { 44 | const source = new ConcatSource() 45 | 46 | // 抽取的公用代码,不使用这个render处理 47 | if (!chunk.entryModule || !chunk.entryModule.resource) { 48 | return bootstrapSource 49 | } 50 | 51 | const filePaths = utils.getExportFilePath(this.extCode, this.compiler.context) 52 | 53 | const resource = getFile(chunk.entryModule.resource) 54 | 55 | const globalRequire = 'require' 56 | 57 | let webpackRequire = `${globalRequire}("${this.getRequirePath(chunk.entryModule.resource)}")` 58 | // 支持独立分包,先这样处理,render hook 添加的不对 59 | if (resource && this.miniLoader.fileTree.getFile(resource).independent) { 60 | webpackRequire = requireCode.toString() + ';\nvar installedModules = {}' 61 | } 62 | 63 | /** 64 | * 计算出 webpack-require 相对改 chunk 的路径 65 | */ 66 | this.targetIsUMD && source.add('(function() {\n') 67 | 68 | source.add(`var webpackRequire = ${webpackRequire};\n`) 69 | 70 | filePaths.includes(resource) && source.add('module.exports = ') 71 | 72 | this.targetIsUMD && source.add('return ') 73 | 74 | source.add(`webpackRequire(\n`) 75 | 76 | source.add(`"${chunk.entryModule.id}",\n`) 77 | 78 | this.setModules(source)(chunk, hash, moduleTemplate, dependencyTemplates) 79 | 80 | source.add(')') 81 | 82 | this.targetIsUMD && source.add('\n})()') 83 | return source 84 | } catch (error) { 85 | console.log(error) 86 | } 87 | } 88 | 89 | setModules (source) { 90 | const globalRequire = 'require' 91 | const mainTemplate = this.compilation.mainTemplate 92 | 93 | return (chunk, hash, moduleTemplate, dependencyTemplates) => { 94 | const modules = this.getDepModules(chunk) 95 | const sourceBody = mainTemplate.hooks.modules.call( 96 | new RawSource(''), 97 | chunk, 98 | hash, 99 | moduleTemplate, 100 | dependencyTemplates 101 | ) 102 | 103 | modules.size && source.add('Object.assign({}, ') 104 | 105 | for (const item of modules) { 106 | source.add(`${globalRequire}("./${item}").modules, `) 107 | } 108 | 109 | source.add(sourceBody) 110 | modules.size && source.add(')') 111 | 112 | return source 113 | } 114 | } 115 | 116 | getDepModules (chunk) { 117 | let groups = chunk.groupsIterable 118 | let modules = new Set() 119 | 120 | if (chunk.hasEntryModule()) { 121 | // 当前 chunk 最后被打包的位置 122 | let jsFilePath = `${chunk.name}.js` 123 | 124 | let file = this.miniLoader.outputUtil.get(jsFilePath) 125 | 126 | for (const chunkGroup of groups) { 127 | for (const { name } of chunkGroup.chunks) { 128 | if (name !== chunk.name) { 129 | // 依赖 chunk 最后被打包的位置 130 | let depFile = this.miniLoader.outputUtil.get(`${name}.js`) 131 | modules.add(utils.relative(file, depFile)) 132 | } 133 | } 134 | } 135 | } 136 | 137 | return modules 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const { 2 | join, 3 | dirname, 4 | extname, 5 | relative, 6 | basename 7 | } = require('path') 8 | const { existsSync } = require('fs') 9 | 10 | exports.camelCase = (str) => { 11 | let words = str.split(/[^a-zA-Z]/) 12 | 13 | return words.reduce((str, val) => { 14 | str += (val[0].toUpperCase() + val.substr(1)) 15 | return str 16 | }, words.shift()) 17 | } 18 | 19 | /** 20 | * 扁平数组 21 | * @param {Array} arr 输入数组 22 | */ 23 | exports.flattenDeep = (arr) => { 24 | while (arr.some(item => Array.isArray(item))) { 25 | arr = [].concat(...arr) 26 | } 27 | return arr 28 | } 29 | 30 | exports.isObject = function (val) { 31 | return typeof val === 'object' && val !== null 32 | } 33 | 34 | exports.relative = (from, to) => { 35 | return './' + relative(dirname(from), to).replace(/\\/g, '/') 36 | } 37 | 38 | exports.join = function (...rest) { 39 | return require('path').posix.join(...rest) 40 | } 41 | 42 | exports.removeExt = (file) => { 43 | return join( 44 | dirname(file), 45 | basename(file, extname(file)) 46 | ) 47 | } 48 | 49 | exports.isEmpty = (obj) => { 50 | return (Array.isArray(obj) && obj.length === 0) || (exports.isObject(obj) && Object.keys(obj).length === 0) 51 | } 52 | 53 | exports.noop = () => {} 54 | 55 | exports.forEachValue = (obj, cb) => { 56 | if (Array.isArray(obj)) { 57 | obj.forEach((item, index) => cb(index, item)) 58 | } else if (exports.isObject(obj)) { 59 | Object.keys(obj).forEach(key => { 60 | cb(key, obj[key]) 61 | }) 62 | } else { 63 | throw Error('参数错误') 64 | } 65 | } 66 | 67 | /** 68 | * @description 获取 app.json plugins 的 export 字段 69 | */ 70 | exports.getExportFilePath = function (appCode, context) { 71 | let filePaths = [] 72 | if (!appCode.plugins) { 73 | return filePaths 74 | } 75 | 76 | Object.keys(appCode.plugins).forEach(fileName => { 77 | if (appCode.plugins[fileName] && appCode.plugins[fileName].export) { 78 | const file = join(context, appCode.plugins[fileName].export) 79 | if (existsSync(file)) { 80 | filePaths.push(file) 81 | } else { 82 | throw new Error(`${file} 不存在,请检查 plugins 中的 export 字段在 ${context} 是否存在`) 83 | } 84 | } 85 | }) 86 | 87 | return filePaths 88 | } 89 | 90 | /** 91 | * 92 | * @param {*} extfile ext 配置 93 | * @param {*} context 执行上下文 94 | * @description 获取 ext.json 95 | */ 96 | exports.getExtPath = function (extfile, context) { 97 | if (extfile === false) { 98 | return '' 99 | } 100 | if (extfile === true) { 101 | return join(context, 'ext.json') 102 | } 103 | if (typeof extfile === 'string') { 104 | // options.extfile 是绝对路径,不需要 join 105 | return extfile 106 | } 107 | } 108 | 109 | --------------------------------------------------------------------------------