├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── dist ├── bundle.js ├── bundle_command.js └── index.d.ts ├── minipack.config.js ├── package.json ├── rollup.config.js ├── src ├── changeConfig.ts ├── command.ts ├── compile │ └── compile.ts ├── config.ts ├── controlFile │ ├── checkFile.ts │ ├── handleAssetsFile.ts │ ├── readFile.ts │ └── watchFile.ts ├── globalConfig.ts ├── index.ts ├── minify │ ├── minifyWxml.ts │ └── minifyWxss.ts ├── typings │ ├── config.d.ts │ └── index.d.ts └── utils │ └── utils.ts ├── test ├── compile.test.js ├── compileCode │ ├── a.ts │ ├── child │ │ ├── child.ts │ │ └── child2 │ │ │ └── child2.ts │ ├── index.ts │ ├── test.wxml │ └── test.wxss └── index.js ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jeremy Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weapp-pack 2 | 3 | 一个利用typescript打包小程序的工具 4 | 5 | 6 | ## 安装 7 | 8 | > npm install weapp-minipack -g 9 | 10 | > yarn global add weapp-minipack 11 | 12 | ## 使用(cli) 13 | 14 | > weapp-minipack -c ./minipack.config.js 15 | 16 | ```javascript 17 | // minipack.config.js 18 | const path = require('path'); 19 | module.exports = { 20 | watchEntry: path.resolve(__dirname, 'miniprogram'), 21 | tsConfigPath: path.resolve(__dirname, 'tsconfig.json'), 22 | outDir: path.resolve(__dirname, 'build'), 23 | isWatch: true, 24 | watchEntry: path.resolve(__dirname, 'src'), 25 | typeRoots: [ 26 | path.resolve(__dirname, './typings'), 27 | path.resolve(__dirname, 'node_modules/@types/node') 28 | ], 29 | } 30 | ``` 31 | 32 | ## config配置 33 | 34 | * env: **string** 当前的运行环境 35 | * watchEntry: **string** 监听文件变化时,复制TS意外的文件时使用的入口文件夹 36 | * outDir: **string** 输出的目录, 37 | * isTs: **string** 代码是否为typescript(待用), 38 | * tsConfigPath: **string** tsconfig.json文件路径 39 | * miniprogramProjectPath: **string** 小程序project.config.json配置文件路径, 40 | * miniprogramProjectConfig: **object** 需要修改 project.config.json 中的数据, 41 | * isWatch: **boolean** 是否开启监听文件变化, 42 | * inpouringEnv: **Object** 注入环境变量 43 | * inpouringEnv.isInpour **boolean 是否注入 44 | * inpouringEnv.files **array string** 需要注入的文件路径 45 | * inpouringEnv.data **string** 需要注入的信息 46 | * plugins 处理其他文件的插件 47 | * esBuildOptions 参考[esbuild](https://esbuild.github.io/api/) 48 | 49 | ## plugins 50 | 你可以使用其他插件去处理不同的文件,例如wxml,wxss代码压缩等, 51 | plugins接受一个对象数组 52 | ```javascript 53 | plugins: [ 54 | { 55 | test: /.*\.wxml$/, 56 | action: ({ data, dataBuf, filePath, copyPath }) => { 57 | //handle function 58 | .... 59 | } 60 | } 61 | ] 62 | ``` 63 | * test: 匹配文件的正则表达式 64 | * action: 处理匹配到的文件函数 65 | action函数会传入一个`IPluginOption`类型的对象 66 | 67 | * IPluginOption.data string 对应文件的字符串表示 68 | * IPluginOption.dataBuf Buffer 对应文件的buffer 69 | * IPluginOption.filePath string 对应文件的路径 70 | * IPluginOption.copyPath string 编译的目标路径 71 | 72 | 在minipack中还内置了wxss压缩插件 73 | ```javascript 74 | const { minifierStyle } = require('weapp_minipack'); 75 | // minipack.config.js 76 | plugins: [ 77 | { 78 | test: /.*\.(wxss)$/, 79 | action: minifierStyle, 80 | } 81 | ] 82 | ``` 83 | 84 | ## 使用(调用实例方法) 85 | 86 | ```javascript 87 | const { Entry } = require('weapp-minipack'); 88 | const path = require('path'); 89 | const pack = new Entry({ configPath: path.resolve(__dirname, '..', 'minipack.config.js') }); 90 | pack.init().start(); 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | "targets": { 7 | "chrome": "80", 8 | "node": "14" 9 | } 10 | } 11 | ], 12 | '@babel/preset-typescript', 13 | ], 14 | }; -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | require('readline'); 8 | var crypto = require('crypto'); 9 | var esbuild = require('esbuild'); 10 | var htmlMinifier = require('html-minifier'); 11 | 12 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 13 | 14 | var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto); 15 | 16 | /** 17 | * default config 18 | */ 19 | const config = { 20 | env: process.env.NODE_ENV || 'none', 21 | watchEntry: '', 22 | outDir: path.resolve(process.cwd(), 'dist'), 23 | isTs: true, 24 | tsConfigPath: '', 25 | miniprogramProjectPath: path.resolve(process.cwd(), '../project.config.json'), 26 | miniprogramProjectConfig: {}, 27 | isWatch: false, 28 | inpouringEnv: { 29 | isInpour: false, 30 | files: [], 31 | data: '', 32 | }, 33 | typeRoots: [], 34 | plugins: [], 35 | }; 36 | 37 | /** 38 | * 判断是否是文件夹 39 | */ 40 | function checkIsDir(filePath) { 41 | return fs.statSync(filePath).isDirectory(); 42 | } 43 | /** 44 | * 判断两个文件大小是否相等 45 | */ 46 | function checkFileIsSame(pathA, pathB) { 47 | const fileA_MD5 = crypto__default['default'].createHash('md5').update(fs.readFileSync(pathA)).digest('hex'); 48 | const fileB_MD5 = crypto__default['default'].createHash('md5').update(fs.readFileSync(pathB)).digest('hex'); 49 | return fileA_MD5 === fileB_MD5; 50 | } 51 | 52 | const EXPLORE_REG = new RegExp(".*.(js|ts)$|.DS_Store"); 53 | const TS_REG = /.*\.ts$/; 54 | 55 | function handleAssetsFile(tmpPath, endPath, plugins) { 56 | let formatData = ''; 57 | for (let x of plugins) { 58 | if (x.test.test(tmpPath) && 59 | fs.existsSync(tmpPath) && 60 | fs.statSync(tmpPath).isFile()) { 61 | const data = fs.readFileSync(tmpPath, { encoding: 'utf-8' }); 62 | const actionData = { 63 | copyDir: endPath, 64 | filePath: tmpPath, 65 | data, 66 | dataBuf: Buffer.alloc(data.length, data), 67 | }; 68 | formatData = x.action(actionData); 69 | break; 70 | } 71 | } 72 | if (formatData) { 73 | fs.writeFileSync(endPath, formatData); 74 | } 75 | else { 76 | copyFile(tmpPath, endPath); 77 | } 78 | return true; 79 | } 80 | 81 | /** 82 | * 读取文件夹 83 | */ 84 | function readDir(filePath) { 85 | if (checkIsDir(filePath)) { 86 | return fs.readdirSync(filePath); 87 | } 88 | else { 89 | return []; 90 | } 91 | } 92 | /** 93 | * 用流的方式复制文件 94 | */ 95 | function copyFile(beginPath, endPath) { 96 | if (fs.existsSync(beginPath) && !checkIsDir(beginPath)) { 97 | const readStream = fs.createReadStream(beginPath); 98 | const writeStream = fs.createWriteStream(endPath); 99 | readStream.pipe(writeStream); 100 | } 101 | } 102 | /** 103 | * 创建文件夹 104 | */ 105 | function createDir(filePath) { 106 | if (!fs.existsSync(filePath)) { 107 | fs.mkdirSync(filePath); 108 | } 109 | } 110 | /** 111 | * 开始编译 112 | */ 113 | async function startCompile(filePath, copyPath, plugins = []) { 114 | // 读取所有文件 115 | const fileArr = readDir(filePath); 116 | for (let x of fileArr) { 117 | const tmpPath = path.resolve(path.resolve(filePath, x)); 118 | let endPath = path.resolve(copyPath, x); 119 | if (checkIsDir(tmpPath)) { 120 | createDir(endPath); 121 | startCompile(tmpPath, endPath, plugins); 122 | } 123 | else if (!EXPLORE_REG.test(endPath) || /\/lib\/.*|\lib\.*/g.test(endPath)) { 124 | if (fs.existsSync(endPath) && checkFileIsSame(tmpPath, endPath)) { 125 | continue; 126 | } 127 | else { 128 | handleAssetsFile(tmpPath, endPath, plugins); 129 | } 130 | } 131 | } 132 | } 133 | /** 134 | * 读取所有ts文件 135 | */ 136 | function readTsFile(filePath, currentPath = '') { 137 | const fileArr = readDir(filePath); 138 | let resultArr = []; 139 | for (let x of fileArr) { 140 | const tmpPath = path.resolve(filePath, x); 141 | const keyPath = `${currentPath}/${x}`; 142 | if (checkIsDir(tmpPath)) { 143 | resultArr = resultArr.concat(readTsFile(tmpPath, keyPath)); 144 | } 145 | else { 146 | if (TS_REG.test(keyPath)) { 147 | resultArr.push(tmpPath); 148 | } 149 | } 150 | } 151 | return resultArr; 152 | } 153 | 154 | const PROJECT_CONFIG_PATH = path.resolve(__dirname, '../project.config.json'); 155 | /** 156 | * 改变小程序配置 157 | */ 158 | function changeMiniprogramConfig(config = {}, configPath = PROJECT_CONFIG_PATH) { 159 | if (fs.existsSync(configPath)) { 160 | let data = fs.readFileSync(configPath, { encoding: 'utf-8' }); 161 | try { 162 | data = JSON.parse(data); 163 | Object.assign(data, config); 164 | data = JSON.stringify(data).replace(/{/g, '{\r\n') 165 | .replace(/}/g, '}\r\n').replace(/,/g, ',\r\n') 166 | .replace(/\[/g, '[\r\n').replace(/\]/g, ']\r\n'); 167 | fs.writeFileSync(configPath, data); 168 | } 169 | catch (err) { 170 | console.error(err); 171 | } 172 | } 173 | } 174 | /** 175 | * 注入环境变量 176 | * @param { String } path 177 | * @param { Array String } configFile 178 | * @param { String } env 179 | */ 180 | function addEnv(rootPath, configFile, env) { 181 | for (let x of configFile) { 182 | const file = path.resolve(rootPath, x); 183 | if (fs.existsSync(file) && fs.statSync(file).isFile()) { 184 | const data = fs.readFileSync(file, { encoding: 'utf-8' }); 185 | console.log(env); 186 | fs.writeFileSync(file, [env, '\r\n', data.replace(new RegExp(env, 'g'), '')].join('')); 187 | } 188 | } 189 | } 190 | 191 | // 对象数组去重 192 | function filterObject(arr) { 193 | const obj = {}; 194 | const result = []; 195 | arr.forEach(val => { 196 | const key = `${val.type}_${val.event}_${val.filename}`; 197 | if (!obj[key]) { 198 | obj[key] = 1; 199 | result.push(val); 200 | } 201 | }); 202 | return result; 203 | } 204 | 205 | /** 206 | * 压缩代码主方法 207 | * @param options esbuild options 208 | * @returns 209 | */ 210 | async function translateCode(options) { 211 | try { 212 | const result = await esbuild.build(options); 213 | return result; 214 | } 215 | catch (err) { 216 | console.log('build err', err); 217 | return false; 218 | } 219 | } 220 | /** 221 | * 编译TS文件 222 | */ 223 | async function actionCompileTsFile(tsFile, rootPath, copyPath, inpourEnv, esBuildOptions) { 224 | console.log('正在编译指定文件'); 225 | console.time('compile'); 226 | for (let compileFile of tsFile) { 227 | const sourchFile = path.resolve(rootPath, compileFile.filename); 228 | let compilePath = path.resolve(copyPath, compileFile.filename).replace(/\\/g, '\/').split('\/'); 229 | compilePath.splice(compilePath.length - 1, 1); 230 | compilePath = compilePath.join('/'); 231 | const result = await translateCode({ 232 | format: 'cjs', 233 | entryPoints: [sourchFile], 234 | minify: true, 235 | outdir: compilePath, 236 | ...esBuildOptions, 237 | }); 238 | console.log(result); 239 | if (inpourEnv.isInpour) { 240 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 241 | } 242 | } 243 | console.log('编译完成'); 244 | console.timeEnd('compile'); 245 | } 246 | /** 247 | * 监听文件开始编译 248 | */ 249 | async function actionCompile(fileArr, option) { 250 | const { rootPath, inpourEnv, miniprogramProjectConfig, miniprogramProjectPath, plugins = [], esBuildOptions, } = option; 251 | let { copyPath, } = option; 252 | // 对象去重 253 | fileArr = filterObject(fileArr); 254 | // 判断是否有文件新增或删除 255 | const isReadName = fileArr.filter(val => val.event === 'rename').length > 0; 256 | // ts之外的文件 257 | const assetsFile = fileArr.filter(val => val.type === 'asset'); 258 | // ts文件 259 | const tsFile = fileArr.filter(val => val.type === 'ts'); 260 | // 有文件新增或删除为重新编译 261 | if (isReadName) { 262 | const fileList = readTsFile(rootPath); 263 | const compileResult = await translateCode({ 264 | format: 'cjs', 265 | entryPoints: fileList, 266 | minify: true, 267 | outdir: copyPath, 268 | ...esBuildOptions, 269 | }); 270 | if (compileResult) { 271 | if (inpourEnv.isInpour) { 272 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 273 | } 274 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 275 | } 276 | // 重新写入文件 277 | startCompile(rootPath, copyPath); 278 | } 279 | else { 280 | // 写入ts文件 281 | if (tsFile.length) { 282 | await actionCompileTsFile(tsFile, rootPath, copyPath, inpourEnv, esBuildOptions); 283 | } 284 | // 写入修改的文件 285 | for (let assetFile of assetsFile) { 286 | handleAssetsFile(path.resolve(rootPath, assetFile.filename), path.resolve(copyPath, assetFile.filename), plugins); 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * 监听文件改动 293 | */ 294 | let changeFileArr = []; 295 | let watchFileTimer = null; 296 | function watchFile(option, during = 500) { 297 | console.log('开始监听文件'); 298 | const { rootPath, } = option; 299 | fs.watch(rootPath, { recursive: true, }, (event, filename) => { 300 | clearTimeout(watchFileTimer); 301 | changeFileArr.push({ 302 | type: EXPLORE_REG.test(filename) ? 'ts' : 'asset', 303 | event, 304 | filename, 305 | }); 306 | watchFileTimer = setTimeout(() => { 307 | actionCompile(changeFileArr, option); 308 | changeFileArr = []; 309 | }, during); 310 | }); 311 | } 312 | 313 | /** 314 | * 压缩HTML CSS文件 315 | * @param filePath 316 | * @param endPath 317 | * @returns 318 | */ 319 | function minifierStyle({ data, }) { 320 | return htmlMinifier.minify(data, { 321 | minifyCSS: true, 322 | removeComments: true, 323 | collapseWhitespace: true, 324 | keepClosingSlash: true, 325 | trimCustomFragments: true, 326 | caseSensitive: true, 327 | }); 328 | } 329 | 330 | function minifyerWxml({ data, }) { 331 | return data.replace(/\n|\s{2,}/g, ' ').replace(/\/\/.*|/g, ''); 332 | } 333 | 334 | class Entry { 335 | constructor(data) { 336 | const { configPath, command } = data; 337 | this.program = command || null; 338 | this.DEFAULT_MINIPACK_CONFIG_PATH = configPath || path.resolve(__dirname, '../minipack.config.js'); 339 | this.config = config; 340 | } 341 | /** 342 | * init project 343 | */ 344 | init() { 345 | this.setConfig(); 346 | return this; 347 | } 348 | /** 349 | * setting bundler config 350 | */ 351 | setConfig() { 352 | let file = this.DEFAULT_MINIPACK_CONFIG_PATH; 353 | if (this.program) { 354 | // get config file 355 | const options = this.program.opts(); 356 | if (!options.config) { 357 | options.config = this.DEFAULT_MINIPACK_CONFIG_PATH; 358 | } 359 | else { 360 | const isFullPath = /^\/.*/.test(options.config); 361 | file = isFullPath ? options.config : path.resolve(process.cwd(), options.config); 362 | } 363 | } 364 | if (fs.existsSync(file) && fs.statSync(file).isFile()) { 365 | try { 366 | let data = require(file); 367 | Object.assign(this.config, data); 368 | if (!this.config.tsConfigPath) 369 | throw new Error('tsConfigPath must defined'); 370 | if (!fs.existsSync(file) || !fs.statSync(file).isFile()) 371 | throw new Error('tsConfigPath path is not found'); 372 | } 373 | catch (err) { 374 | throw new Error(err.toString()); 375 | } 376 | } 377 | else { 378 | throw new Error(`config file ${file} is not defined`); 379 | } 380 | } 381 | /** 382 | * start build 383 | */ 384 | async start() { 385 | const { watchEntry, outDir, inpouringEnv, esBuildOptions, } = this.config; 386 | console.log('compile start'); 387 | const fileList = readTsFile(watchEntry); 388 | const compileResult = await translateCode({ 389 | format: 'cjs', 390 | entryPoints: fileList, 391 | minify: true, 392 | outdir: outDir, 393 | ...esBuildOptions, 394 | }); 395 | // const result = childProcess.spawnSync(`tsc`,[`--project`, tsConfigPath, '--outDir', outDir,], { shell: true, }); 396 | if (compileResult) { 397 | console.log('compile finished'); 398 | if (inpouringEnv.isInpour) { 399 | console.log('start inpour data'); 400 | addEnv(outDir, inpouringEnv.files, inpouringEnv.data); 401 | console.log('inpour finished'); 402 | } 403 | await this.copyFile(); 404 | this.watchFile(); 405 | } 406 | else { 407 | return; 408 | } 409 | } 410 | /** 411 | * copy other asset files 412 | */ 413 | copyFile() { 414 | return new Promise(async (truly) => { 415 | const { watchEntry, outDir, miniprogramProjectConfig, miniprogramProjectPath, plugins, } = this.config; 416 | console.log('start copy asset files'); 417 | await startCompile(watchEntry, outDir, plugins); 418 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 419 | console.log('copy assets success'); 420 | truly(true); 421 | }); 422 | } 423 | /** 424 | * watchFile 425 | */ 426 | watchFile() { 427 | const { isWatch, watchEntry, outDir, tsConfigPath, miniprogramProjectConfig, miniprogramProjectPath, inpouringEnv, typeRoots, plugins, esBuildOptions = {}, } = this.config; 428 | if (isWatch) { 429 | const watchOption = { 430 | rootPath: watchEntry, 431 | copyPath: outDir, 432 | tsconfigPath: tsConfigPath, 433 | inpourEnv: inpouringEnv, 434 | miniprogramProjectPath, 435 | miniprogramProjectConfig, 436 | typingDirPath: typeRoots, 437 | plugins, 438 | esBuildOptions, 439 | }; 440 | watchFile(watchOption); 441 | } 442 | } 443 | } 444 | const minifyStyle = minifierStyle; 445 | const minifyWxml = minifyerWxml; 446 | 447 | exports.Entry = Entry; 448 | exports.minifyStyle = minifyStyle; 449 | exports.minifyWxml = minifyWxml; 450 | -------------------------------------------------------------------------------- /dist/bundle_command.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | require('readline'); 7 | var crypto = require('crypto'); 8 | var esbuild = require('esbuild'); 9 | require('html-minifier'); 10 | var events = require('events'); 11 | var child_process = require('child_process'); 12 | 13 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 14 | 15 | var path__default = /*#__PURE__*/_interopDefaultLegacy(path); 16 | var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); 17 | var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto); 18 | var events__default = /*#__PURE__*/_interopDefaultLegacy(events); 19 | var child_process__default = /*#__PURE__*/_interopDefaultLegacy(child_process); 20 | 21 | /** 22 | * default config 23 | */ 24 | const config = { 25 | env: process.env.NODE_ENV || 'none', 26 | watchEntry: '', 27 | outDir: path.resolve(process.cwd(), 'dist'), 28 | isTs: true, 29 | tsConfigPath: '', 30 | miniprogramProjectPath: path.resolve(process.cwd(), '../project.config.json'), 31 | miniprogramProjectConfig: {}, 32 | isWatch: false, 33 | inpouringEnv: { 34 | isInpour: false, 35 | files: [], 36 | data: '', 37 | }, 38 | typeRoots: [], 39 | plugins: [], 40 | }; 41 | 42 | /** 43 | * 判断是否是文件夹 44 | */ 45 | function checkIsDir(filePath) { 46 | return fs.statSync(filePath).isDirectory(); 47 | } 48 | /** 49 | * 判断两个文件大小是否相等 50 | */ 51 | function checkFileIsSame(pathA, pathB) { 52 | const fileA_MD5 = crypto__default['default'].createHash('md5').update(fs.readFileSync(pathA)).digest('hex'); 53 | const fileB_MD5 = crypto__default['default'].createHash('md5').update(fs.readFileSync(pathB)).digest('hex'); 54 | return fileA_MD5 === fileB_MD5; 55 | } 56 | 57 | const EXPLORE_REG = new RegExp(".*.(js|ts)$|.DS_Store"); 58 | const TS_REG = /.*\.ts$/; 59 | 60 | function handleAssetsFile(tmpPath, endPath, plugins) { 61 | let formatData = ''; 62 | for (let x of plugins) { 63 | if (x.test.test(tmpPath) && 64 | fs.existsSync(tmpPath) && 65 | fs.statSync(tmpPath).isFile()) { 66 | const data = fs.readFileSync(tmpPath, { encoding: 'utf-8' }); 67 | const actionData = { 68 | copyDir: endPath, 69 | filePath: tmpPath, 70 | data, 71 | dataBuf: Buffer.alloc(data.length, data), 72 | }; 73 | formatData = x.action(actionData); 74 | break; 75 | } 76 | } 77 | if (formatData) { 78 | fs.writeFileSync(endPath, formatData); 79 | } 80 | else { 81 | copyFile(tmpPath, endPath); 82 | } 83 | return true; 84 | } 85 | 86 | /** 87 | * 读取文件夹 88 | */ 89 | function readDir(filePath) { 90 | if (checkIsDir(filePath)) { 91 | return fs.readdirSync(filePath); 92 | } 93 | else { 94 | return []; 95 | } 96 | } 97 | /** 98 | * 用流的方式复制文件 99 | */ 100 | function copyFile(beginPath, endPath) { 101 | if (fs.existsSync(beginPath) && !checkIsDir(beginPath)) { 102 | const readStream = fs.createReadStream(beginPath); 103 | const writeStream = fs.createWriteStream(endPath); 104 | readStream.pipe(writeStream); 105 | } 106 | } 107 | /** 108 | * 创建文件夹 109 | */ 110 | function createDir(filePath) { 111 | if (!fs.existsSync(filePath)) { 112 | fs.mkdirSync(filePath); 113 | } 114 | } 115 | /** 116 | * 开始编译 117 | */ 118 | async function startCompile(filePath, copyPath, plugins = []) { 119 | // 读取所有文件 120 | const fileArr = readDir(filePath); 121 | for (let x of fileArr) { 122 | const tmpPath = path.resolve(path.resolve(filePath, x)); 123 | let endPath = path.resolve(copyPath, x); 124 | if (checkIsDir(tmpPath)) { 125 | createDir(endPath); 126 | startCompile(tmpPath, endPath, plugins); 127 | } 128 | else if (!EXPLORE_REG.test(endPath) || /\/lib\/.*|\lib\.*/g.test(endPath)) { 129 | if (fs.existsSync(endPath) && checkFileIsSame(tmpPath, endPath)) { 130 | continue; 131 | } 132 | else { 133 | handleAssetsFile(tmpPath, endPath, plugins); 134 | } 135 | } 136 | } 137 | } 138 | /** 139 | * 读取所有ts文件 140 | */ 141 | function readTsFile(filePath, currentPath = '') { 142 | const fileArr = readDir(filePath); 143 | let resultArr = []; 144 | for (let x of fileArr) { 145 | const tmpPath = path.resolve(filePath, x); 146 | const keyPath = `${currentPath}/${x}`; 147 | if (checkIsDir(tmpPath)) { 148 | resultArr = resultArr.concat(readTsFile(tmpPath, keyPath)); 149 | } 150 | else { 151 | if (TS_REG.test(keyPath)) { 152 | resultArr.push(tmpPath); 153 | } 154 | } 155 | } 156 | return resultArr; 157 | } 158 | 159 | const PROJECT_CONFIG_PATH = path.resolve(__dirname, '../project.config.json'); 160 | /** 161 | * 改变小程序配置 162 | */ 163 | function changeMiniprogramConfig(config = {}, configPath = PROJECT_CONFIG_PATH) { 164 | if (fs.existsSync(configPath)) { 165 | let data = fs.readFileSync(configPath, { encoding: 'utf-8' }); 166 | try { 167 | data = JSON.parse(data); 168 | Object.assign(data, config); 169 | data = JSON.stringify(data).replace(/{/g, '{\r\n') 170 | .replace(/}/g, '}\r\n').replace(/,/g, ',\r\n') 171 | .replace(/\[/g, '[\r\n').replace(/\]/g, ']\r\n'); 172 | fs.writeFileSync(configPath, data); 173 | } 174 | catch (err) { 175 | console.error(err); 176 | } 177 | } 178 | } 179 | /** 180 | * 注入环境变量 181 | * @param { String } path 182 | * @param { Array String } configFile 183 | * @param { String } env 184 | */ 185 | function addEnv(rootPath, configFile, env) { 186 | for (let x of configFile) { 187 | const file = path.resolve(rootPath, x); 188 | if (fs.existsSync(file) && fs.statSync(file).isFile()) { 189 | const data = fs.readFileSync(file, { encoding: 'utf-8' }); 190 | console.log(env); 191 | fs.writeFileSync(file, [env, '\r\n', data.replace(new RegExp(env, 'g'), '')].join('')); 192 | } 193 | } 194 | } 195 | 196 | // 对象数组去重 197 | function filterObject(arr) { 198 | const obj = {}; 199 | const result = []; 200 | arr.forEach(val => { 201 | const key = `${val.type}_${val.event}_${val.filename}`; 202 | if (!obj[key]) { 203 | obj[key] = 1; 204 | result.push(val); 205 | } 206 | }); 207 | return result; 208 | } 209 | 210 | /** 211 | * 压缩代码主方法 212 | * @param options esbuild options 213 | * @returns 214 | */ 215 | async function translateCode(options) { 216 | try { 217 | const result = await esbuild.build(options); 218 | return result; 219 | } 220 | catch (err) { 221 | console.log('build err', err); 222 | return false; 223 | } 224 | } 225 | /** 226 | * 编译TS文件 227 | */ 228 | async function actionCompileTsFile(tsFile, rootPath, copyPath, inpourEnv, esBuildOptions) { 229 | console.log('正在编译指定文件'); 230 | console.time('compile'); 231 | for (let compileFile of tsFile) { 232 | const sourchFile = path.resolve(rootPath, compileFile.filename); 233 | let compilePath = path.resolve(copyPath, compileFile.filename).replace(/\\/g, '\/').split('\/'); 234 | compilePath.splice(compilePath.length - 1, 1); 235 | compilePath = compilePath.join('/'); 236 | const result = await translateCode({ 237 | format: 'cjs', 238 | entryPoints: [sourchFile], 239 | minify: true, 240 | outdir: compilePath, 241 | ...esBuildOptions, 242 | }); 243 | console.log(result); 244 | if (inpourEnv.isInpour) { 245 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 246 | } 247 | } 248 | console.log('编译完成'); 249 | console.timeEnd('compile'); 250 | } 251 | /** 252 | * 监听文件开始编译 253 | */ 254 | async function actionCompile(fileArr, option) { 255 | const { rootPath, inpourEnv, miniprogramProjectConfig, miniprogramProjectPath, plugins = [], esBuildOptions, } = option; 256 | let { copyPath, } = option; 257 | // 对象去重 258 | fileArr = filterObject(fileArr); 259 | // 判断是否有文件新增或删除 260 | const isReadName = fileArr.filter(val => val.event === 'rename').length > 0; 261 | // ts之外的文件 262 | const assetsFile = fileArr.filter(val => val.type === 'asset'); 263 | // ts文件 264 | const tsFile = fileArr.filter(val => val.type === 'ts'); 265 | // 有文件新增或删除为重新编译 266 | if (isReadName) { 267 | const fileList = readTsFile(rootPath); 268 | const compileResult = await translateCode({ 269 | format: 'cjs', 270 | entryPoints: fileList, 271 | minify: true, 272 | outdir: copyPath, 273 | ...esBuildOptions, 274 | }); 275 | if (compileResult) { 276 | if (inpourEnv.isInpour) { 277 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 278 | } 279 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 280 | } 281 | // 重新写入文件 282 | startCompile(rootPath, copyPath); 283 | } 284 | else { 285 | // 写入ts文件 286 | if (tsFile.length) { 287 | await actionCompileTsFile(tsFile, rootPath, copyPath, inpourEnv, esBuildOptions); 288 | } 289 | // 写入修改的文件 290 | for (let assetFile of assetsFile) { 291 | handleAssetsFile(path.resolve(rootPath, assetFile.filename), path.resolve(copyPath, assetFile.filename), plugins); 292 | } 293 | } 294 | } 295 | 296 | /** 297 | * 监听文件改动 298 | */ 299 | let changeFileArr = []; 300 | let watchFileTimer = null; 301 | function watchFile(option, during = 500) { 302 | console.log('开始监听文件'); 303 | const { rootPath, } = option; 304 | fs.watch(rootPath, { recursive: true, }, (event, filename) => { 305 | clearTimeout(watchFileTimer); 306 | changeFileArr.push({ 307 | type: EXPLORE_REG.test(filename) ? 'ts' : 'asset', 308 | event, 309 | filename, 310 | }); 311 | watchFileTimer = setTimeout(() => { 312 | actionCompile(changeFileArr, option); 313 | changeFileArr = []; 314 | }, during); 315 | }); 316 | } 317 | 318 | class Entry { 319 | constructor(data) { 320 | const { configPath, command } = data; 321 | this.program = command || null; 322 | this.DEFAULT_MINIPACK_CONFIG_PATH = configPath || path.resolve(__dirname, '../minipack.config.js'); 323 | this.config = config; 324 | } 325 | /** 326 | * init project 327 | */ 328 | init() { 329 | this.setConfig(); 330 | return this; 331 | } 332 | /** 333 | * setting bundler config 334 | */ 335 | setConfig() { 336 | let file = this.DEFAULT_MINIPACK_CONFIG_PATH; 337 | if (this.program) { 338 | // get config file 339 | const options = this.program.opts(); 340 | if (!options.config) { 341 | options.config = this.DEFAULT_MINIPACK_CONFIG_PATH; 342 | } 343 | else { 344 | const isFullPath = /^\/.*/.test(options.config); 345 | file = isFullPath ? options.config : path.resolve(process.cwd(), options.config); 346 | } 347 | } 348 | if (fs.existsSync(file) && fs.statSync(file).isFile()) { 349 | try { 350 | let data = require(file); 351 | Object.assign(this.config, data); 352 | if (!this.config.tsConfigPath) 353 | throw new Error('tsConfigPath must defined'); 354 | if (!fs.existsSync(file) || !fs.statSync(file).isFile()) 355 | throw new Error('tsConfigPath path is not found'); 356 | } 357 | catch (err) { 358 | throw new Error(err.toString()); 359 | } 360 | } 361 | else { 362 | throw new Error(`config file ${file} is not defined`); 363 | } 364 | } 365 | /** 366 | * start build 367 | */ 368 | async start() { 369 | const { watchEntry, outDir, inpouringEnv, esBuildOptions, } = this.config; 370 | console.log('compile start'); 371 | const fileList = readTsFile(watchEntry); 372 | const compileResult = await translateCode({ 373 | format: 'cjs', 374 | entryPoints: fileList, 375 | minify: true, 376 | outdir: outDir, 377 | ...esBuildOptions, 378 | }); 379 | // const result = childProcess.spawnSync(`tsc`,[`--project`, tsConfigPath, '--outDir', outDir,], { shell: true, }); 380 | if (compileResult) { 381 | console.log('compile finished'); 382 | if (inpouringEnv.isInpour) { 383 | console.log('start inpour data'); 384 | addEnv(outDir, inpouringEnv.files, inpouringEnv.data); 385 | console.log('inpour finished'); 386 | } 387 | await this.copyFile(); 388 | this.watchFile(); 389 | } 390 | else { 391 | return; 392 | } 393 | } 394 | /** 395 | * copy other asset files 396 | */ 397 | copyFile() { 398 | return new Promise(async (truly) => { 399 | const { watchEntry, outDir, miniprogramProjectConfig, miniprogramProjectPath, plugins, } = this.config; 400 | console.log('start copy asset files'); 401 | await startCompile(watchEntry, outDir, plugins); 402 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 403 | console.log('copy assets success'); 404 | truly(true); 405 | }); 406 | } 407 | /** 408 | * watchFile 409 | */ 410 | watchFile() { 411 | const { isWatch, watchEntry, outDir, tsConfigPath, miniprogramProjectConfig, miniprogramProjectPath, inpouringEnv, typeRoots, plugins, esBuildOptions = {}, } = this.config; 412 | if (isWatch) { 413 | const watchOption = { 414 | rootPath: watchEntry, 415 | copyPath: outDir, 416 | tsconfigPath: tsConfigPath, 417 | inpourEnv: inpouringEnv, 418 | miniprogramProjectPath, 419 | miniprogramProjectConfig, 420 | typingDirPath: typeRoots, 421 | plugins, 422 | esBuildOptions, 423 | }; 424 | watchFile(watchOption); 425 | } 426 | } 427 | } 428 | 429 | function commonjsRequire () { 430 | throw new Error('Dynamic requires are not currently supported by rollup-plugin-commonjs'); 431 | } 432 | 433 | function createCommonjsModule(fn, module) { 434 | return module = { exports: {} }, fn(module, module.exports), module.exports; 435 | } 436 | 437 | var commander = createCommonjsModule(function (module, exports) { 438 | /** 439 | * Module dependencies. 440 | */ 441 | 442 | const EventEmitter = events__default['default'].EventEmitter; 443 | 444 | 445 | 446 | 447 | // @ts-check 448 | 449 | // Although this is a class, methods are static in style to allow override using subclass or just functions. 450 | class Help { 451 | constructor() { 452 | this.helpWidth = undefined; 453 | this.sortSubcommands = false; 454 | this.sortOptions = false; 455 | } 456 | 457 | /** 458 | * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one. 459 | * 460 | * @param {Command} cmd 461 | * @returns {Command[]} 462 | */ 463 | 464 | visibleCommands(cmd) { 465 | const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); 466 | if (cmd._hasImplicitHelpCommand()) { 467 | // Create a command matching the implicit help command. 468 | const args = cmd._helpCommandnameAndArgs.split(/ +/); 469 | const helpCommand = cmd.createCommand(args.shift()) 470 | .helpOption(false); 471 | helpCommand.description(cmd._helpCommandDescription); 472 | helpCommand._parseExpectedArgs(args); 473 | visibleCommands.push(helpCommand); 474 | } 475 | if (this.sortSubcommands) { 476 | visibleCommands.sort((a, b) => { 477 | return a.name().localeCompare(b.name()); 478 | }); 479 | } 480 | return visibleCommands; 481 | } 482 | 483 | /** 484 | * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. 485 | * 486 | * @param {Command} cmd 487 | * @returns {Option[]} 488 | */ 489 | 490 | visibleOptions(cmd) { 491 | const visibleOptions = cmd.options.filter((option) => !option.hidden); 492 | // Implicit help 493 | const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag); 494 | const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag); 495 | if (showShortHelpFlag || showLongHelpFlag) { 496 | let helpOption; 497 | if (!showShortHelpFlag) { 498 | helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription); 499 | } else if (!showLongHelpFlag) { 500 | helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription); 501 | } else { 502 | helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription); 503 | } 504 | visibleOptions.push(helpOption); 505 | } 506 | if (this.sortOptions) { 507 | const getSortKey = (option) => { 508 | // WYSIWYG for order displayed in help with short before long, no special handling for negated. 509 | return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, ''); 510 | }; 511 | visibleOptions.sort((a, b) => { 512 | return getSortKey(a).localeCompare(getSortKey(b)); 513 | }); 514 | } 515 | return visibleOptions; 516 | } 517 | 518 | /** 519 | * Get an array of the arguments which have descriptions. 520 | * 521 | * @param {Command} cmd 522 | * @returns {{ term: string, description:string }[]} 523 | */ 524 | 525 | visibleArguments(cmd) { 526 | if (cmd._argsDescription && cmd._args.length) { 527 | return cmd._args.map((argument) => { 528 | return { term: argument.name, description: cmd._argsDescription[argument.name] || '' }; 529 | }, 0); 530 | } 531 | return []; 532 | } 533 | 534 | /** 535 | * Get the command term to show in the list of subcommands. 536 | * 537 | * @param {Command} cmd 538 | * @returns {string} 539 | */ 540 | 541 | subcommandTerm(cmd) { 542 | // Legacy. Ignores custom usage string, and nested commands. 543 | const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); 544 | return cmd._name + 545 | (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') + 546 | (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option 547 | (args ? ' ' + args : ''); 548 | } 549 | 550 | /** 551 | * Get the option term to show in the list of options. 552 | * 553 | * @param {Option} option 554 | * @returns {string} 555 | */ 556 | 557 | optionTerm(option) { 558 | return option.flags; 559 | } 560 | 561 | /** 562 | * Get the longest command term length. 563 | * 564 | * @param {Command} cmd 565 | * @param {Help} helper 566 | * @returns {number} 567 | */ 568 | 569 | longestSubcommandTermLength(cmd, helper) { 570 | return helper.visibleCommands(cmd).reduce((max, command) => { 571 | return Math.max(max, helper.subcommandTerm(command).length); 572 | }, 0); 573 | }; 574 | 575 | /** 576 | * Get the longest option term length. 577 | * 578 | * @param {Command} cmd 579 | * @param {Help} helper 580 | * @returns {number} 581 | */ 582 | 583 | longestOptionTermLength(cmd, helper) { 584 | return helper.visibleOptions(cmd).reduce((max, option) => { 585 | return Math.max(max, helper.optionTerm(option).length); 586 | }, 0); 587 | }; 588 | 589 | /** 590 | * Get the longest argument term length. 591 | * 592 | * @param {Command} cmd 593 | * @param {Help} helper 594 | * @returns {number} 595 | */ 596 | 597 | longestArgumentTermLength(cmd, helper) { 598 | return helper.visibleArguments(cmd).reduce((max, argument) => { 599 | return Math.max(max, argument.term.length); 600 | }, 0); 601 | }; 602 | 603 | /** 604 | * Get the command usage to be displayed at the top of the built-in help. 605 | * 606 | * @param {Command} cmd 607 | * @returns {string} 608 | */ 609 | 610 | commandUsage(cmd) { 611 | // Usage 612 | let cmdName = cmd._name; 613 | if (cmd._aliases[0]) { 614 | cmdName = cmdName + '|' + cmd._aliases[0]; 615 | } 616 | let parentCmdNames = ''; 617 | for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) { 618 | parentCmdNames = parentCmd.name() + ' ' + parentCmdNames; 619 | } 620 | return parentCmdNames + cmdName + ' ' + cmd.usage(); 621 | } 622 | 623 | /** 624 | * Get the description for the command. 625 | * 626 | * @param {Command} cmd 627 | * @returns {string} 628 | */ 629 | 630 | commandDescription(cmd) { 631 | // @ts-ignore: overloaded return type 632 | return cmd.description(); 633 | } 634 | 635 | /** 636 | * Get the command description to show in the list of subcommands. 637 | * 638 | * @param {Command} cmd 639 | * @returns {string} 640 | */ 641 | 642 | subcommandDescription(cmd) { 643 | // @ts-ignore: overloaded return type 644 | return cmd.description(); 645 | } 646 | 647 | /** 648 | * Get the option description to show in the list of options. 649 | * 650 | * @param {Option} option 651 | * @return {string} 652 | */ 653 | 654 | optionDescription(option) { 655 | if (option.negate) { 656 | return option.description; 657 | } 658 | const extraInfo = []; 659 | if (option.argChoices) { 660 | extraInfo.push( 661 | // use stringify to match the display of the default value 662 | `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`); 663 | } 664 | if (option.defaultValue !== undefined) { 665 | extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`); 666 | } 667 | if (extraInfo.length > 0) { 668 | return `${option.description} (${extraInfo.join(', ')})`; 669 | } 670 | return option.description; 671 | }; 672 | 673 | /** 674 | * Generate the built-in help text. 675 | * 676 | * @param {Command} cmd 677 | * @param {Help} helper 678 | * @returns {string} 679 | */ 680 | 681 | formatHelp(cmd, helper) { 682 | const termWidth = helper.padWidth(cmd, helper); 683 | const helpWidth = helper.helpWidth || 80; 684 | const itemIndentWidth = 2; 685 | const itemSeparatorWidth = 2; // between term and description 686 | function formatItem(term, description) { 687 | if (description) { 688 | const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`; 689 | return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth); 690 | } 691 | return term; 692 | } function formatList(textArray) { 693 | return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth)); 694 | } 695 | 696 | // Usage 697 | let output = [`Usage: ${helper.commandUsage(cmd)}`, '']; 698 | 699 | // Description 700 | const commandDescription = helper.commandDescription(cmd); 701 | if (commandDescription.length > 0) { 702 | output = output.concat([commandDescription, '']); 703 | } 704 | 705 | // Arguments 706 | const argumentList = helper.visibleArguments(cmd).map((argument) => { 707 | return formatItem(argument.term, argument.description); 708 | }); 709 | if (argumentList.length > 0) { 710 | output = output.concat(['Arguments:', formatList(argumentList), '']); 711 | } 712 | 713 | // Options 714 | const optionList = helper.visibleOptions(cmd).map((option) => { 715 | return formatItem(helper.optionTerm(option), helper.optionDescription(option)); 716 | }); 717 | if (optionList.length > 0) { 718 | output = output.concat(['Options:', formatList(optionList), '']); 719 | } 720 | 721 | // Commands 722 | const commandList = helper.visibleCommands(cmd).map((cmd) => { 723 | return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd)); 724 | }); 725 | if (commandList.length > 0) { 726 | output = output.concat(['Commands:', formatList(commandList), '']); 727 | } 728 | 729 | return output.join('\n'); 730 | } 731 | 732 | /** 733 | * Calculate the pad width from the maximum term length. 734 | * 735 | * @param {Command} cmd 736 | * @param {Help} helper 737 | * @returns {number} 738 | */ 739 | 740 | padWidth(cmd, helper) { 741 | return Math.max( 742 | helper.longestOptionTermLength(cmd, helper), 743 | helper.longestSubcommandTermLength(cmd, helper), 744 | helper.longestArgumentTermLength(cmd, helper) 745 | ); 746 | }; 747 | 748 | /** 749 | * Wrap the given string to width characters per line, with lines after the first indented. 750 | * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted. 751 | * 752 | * @param {string} str 753 | * @param {number} width 754 | * @param {number} indent 755 | * @param {number} [minColumnWidth=40] 756 | * @return {string} 757 | * 758 | */ 759 | 760 | wrap(str, width, indent, minColumnWidth = 40) { 761 | // Detect manually wrapped and indented strings by searching for line breaks 762 | // followed by multiple spaces/tabs. 763 | if (str.match(/[\n]\s+/)) return str; 764 | // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line). 765 | const columnWidth = width - indent; 766 | if (columnWidth < minColumnWidth) return str; 767 | 768 | const leadingStr = str.substr(0, indent); 769 | const columnText = str.substr(indent); 770 | 771 | const indentString = ' '.repeat(indent); 772 | const regex = new RegExp('.{1,' + (columnWidth - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); 773 | const lines = columnText.match(regex) || []; 774 | return leadingStr + lines.map((line, i) => { 775 | if (line.slice(-1) === '\n') { 776 | line = line.slice(0, line.length - 1); 777 | } 778 | return ((i > 0) ? indentString : '') + line.trimRight(); 779 | }).join('\n'); 780 | } 781 | } 782 | 783 | class Option { 784 | /** 785 | * Initialize a new `Option` with the given `flags` and `description`. 786 | * 787 | * @param {string} flags 788 | * @param {string} [description] 789 | */ 790 | 791 | constructor(flags, description) { 792 | this.flags = flags; 793 | this.description = description || ''; 794 | 795 | this.required = flags.includes('<'); // A value must be supplied when the option is specified. 796 | this.optional = flags.includes('['); // A value is optional when the option is specified. 797 | // variadic test ignores et al which might be used to describe custom splitting of single argument 798 | this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values. 799 | this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line. 800 | const optionFlags = _parseOptionFlags(flags); 801 | this.short = optionFlags.shortFlag; 802 | this.long = optionFlags.longFlag; 803 | this.negate = false; 804 | if (this.long) { 805 | this.negate = this.long.startsWith('--no-'); 806 | } 807 | this.defaultValue = undefined; 808 | this.defaultValueDescription = undefined; 809 | this.parseArg = undefined; 810 | this.hidden = false; 811 | this.argChoices = undefined; 812 | } 813 | 814 | /** 815 | * Set the default value, and optionally supply the description to be displayed in the help. 816 | * 817 | * @param {any} value 818 | * @param {string} [description] 819 | * @return {Option} 820 | */ 821 | 822 | default(value, description) { 823 | this.defaultValue = value; 824 | this.defaultValueDescription = description; 825 | return this; 826 | }; 827 | 828 | /** 829 | * Set the custom handler for processing CLI option arguments into option values. 830 | * 831 | * @param {Function} [fn] 832 | * @return {Option} 833 | */ 834 | 835 | argParser(fn) { 836 | this.parseArg = fn; 837 | return this; 838 | }; 839 | 840 | /** 841 | * Whether the option is mandatory and must have a value after parsing. 842 | * 843 | * @param {boolean} [mandatory=true] 844 | * @return {Option} 845 | */ 846 | 847 | makeOptionMandatory(mandatory = true) { 848 | this.mandatory = !!mandatory; 849 | return this; 850 | }; 851 | 852 | /** 853 | * Hide option in help. 854 | * 855 | * @param {boolean} [hide=true] 856 | * @return {Option} 857 | */ 858 | 859 | hideHelp(hide = true) { 860 | this.hidden = !!hide; 861 | return this; 862 | }; 863 | 864 | /** 865 | * @api private 866 | */ 867 | 868 | _concatValue(value, previous) { 869 | if (previous === this.defaultValue || !Array.isArray(previous)) { 870 | return [value]; 871 | } 872 | 873 | return previous.concat(value); 874 | } 875 | 876 | /** 877 | * Only allow option value to be one of choices. 878 | * 879 | * @param {string[]} values 880 | * @return {Option} 881 | */ 882 | 883 | choices(values) { 884 | this.argChoices = values; 885 | this.parseArg = (arg, previous) => { 886 | if (!values.includes(arg)) { 887 | throw new InvalidOptionArgumentError(`Allowed choices are ${values.join(', ')}.`); 888 | } 889 | if (this.variadic) { 890 | return this._concatValue(arg, previous); 891 | } 892 | return arg; 893 | }; 894 | return this; 895 | }; 896 | 897 | /** 898 | * Return option name. 899 | * 900 | * @return {string} 901 | */ 902 | 903 | name() { 904 | if (this.long) { 905 | return this.long.replace(/^--/, ''); 906 | } 907 | return this.short.replace(/^-/, ''); 908 | }; 909 | 910 | /** 911 | * Return option name, in a camelcase format that can be used 912 | * as a object attribute key. 913 | * 914 | * @return {string} 915 | * @api private 916 | */ 917 | 918 | attributeName() { 919 | return camelcase(this.name().replace(/^no-/, '')); 920 | }; 921 | 922 | /** 923 | * Check if `arg` matches the short or long flag. 924 | * 925 | * @param {string} arg 926 | * @return {boolean} 927 | * @api private 928 | */ 929 | 930 | is(arg) { 931 | return this.short === arg || this.long === arg; 932 | }; 933 | } 934 | 935 | /** 936 | * CommanderError class 937 | * @class 938 | */ 939 | class CommanderError extends Error { 940 | /** 941 | * Constructs the CommanderError class 942 | * @param {number} exitCode suggested exit code which could be used with process.exit 943 | * @param {string} code an id string representing the error 944 | * @param {string} message human-readable description of the error 945 | * @constructor 946 | */ 947 | constructor(exitCode, code, message) { 948 | super(message); 949 | // properly capture stack trace in Node.js 950 | Error.captureStackTrace(this, this.constructor); 951 | this.name = this.constructor.name; 952 | this.code = code; 953 | this.exitCode = exitCode; 954 | this.nestedError = undefined; 955 | } 956 | } 957 | 958 | /** 959 | * InvalidOptionArgumentError class 960 | * @class 961 | */ 962 | class InvalidOptionArgumentError extends CommanderError { 963 | /** 964 | * Constructs the InvalidOptionArgumentError class 965 | * @param {string} [message] explanation of why argument is invalid 966 | * @constructor 967 | */ 968 | constructor(message) { 969 | super(1, 'commander.invalidOptionArgument', message); 970 | // properly capture stack trace in Node.js 971 | Error.captureStackTrace(this, this.constructor); 972 | this.name = this.constructor.name; 973 | } 974 | } 975 | 976 | class Command extends EventEmitter { 977 | /** 978 | * Initialize a new `Command`. 979 | * 980 | * @param {string} [name] 981 | */ 982 | 983 | constructor(name) { 984 | super(); 985 | this.commands = []; 986 | this.options = []; 987 | this.parent = null; 988 | this._allowUnknownOption = false; 989 | this._allowExcessArguments = true; 990 | this._args = []; 991 | this.rawArgs = null; 992 | this._scriptPath = null; 993 | this._name = name || ''; 994 | this._optionValues = {}; 995 | this._storeOptionsAsProperties = false; 996 | this._actionResults = []; 997 | this._actionHandler = null; 998 | this._executableHandler = false; 999 | this._executableFile = null; // custom name for executable 1000 | this._defaultCommandName = null; 1001 | this._exitCallback = null; 1002 | this._aliases = []; 1003 | this._combineFlagAndOptionalValue = true; 1004 | this._description = ''; 1005 | this._argsDescription = undefined; 1006 | this._enablePositionalOptions = false; 1007 | this._passThroughOptions = false; 1008 | 1009 | // see .configureOutput() for docs 1010 | this._outputConfiguration = { 1011 | writeOut: (str) => process.stdout.write(str), 1012 | writeErr: (str) => process.stderr.write(str), 1013 | getOutHelpWidth: () => process.stdout.isTTY ? process.stdout.columns : undefined, 1014 | getErrHelpWidth: () => process.stderr.isTTY ? process.stderr.columns : undefined, 1015 | outputError: (str, write) => write(str) 1016 | }; 1017 | 1018 | this._hidden = false; 1019 | this._hasHelpOption = true; 1020 | this._helpFlags = '-h, --help'; 1021 | this._helpDescription = 'display help for command'; 1022 | this._helpShortFlag = '-h'; 1023 | this._helpLongFlag = '--help'; 1024 | this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false 1025 | this._helpCommandName = 'help'; 1026 | this._helpCommandnameAndArgs = 'help [command]'; 1027 | this._helpCommandDescription = 'display help for command'; 1028 | this._helpConfiguration = {}; 1029 | } 1030 | 1031 | /** 1032 | * Define a command. 1033 | * 1034 | * There are two styles of command: pay attention to where to put the description. 1035 | * 1036 | * Examples: 1037 | * 1038 | * // Command implemented using action handler (description is supplied separately to `.command`) 1039 | * program 1040 | * .command('clone [destination]') 1041 | * .description('clone a repository into a newly created directory') 1042 | * .action((source, destination) => { 1043 | * console.log('clone command called'); 1044 | * }); 1045 | * 1046 | * // Command implemented using separate executable file (description is second parameter to `.command`) 1047 | * program 1048 | * .command('start ', 'start named service') 1049 | * .command('stop [service]', 'stop named service, or all if no name supplied'); 1050 | * 1051 | * @param {string} nameAndArgs - command name and arguments, args are `` or `[optional]` and last may also be `variadic...` 1052 | * @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable) 1053 | * @param {Object} [execOpts] - configuration options (for executable) 1054 | * @return {Command} returns new command for action handler, or `this` for executable command 1055 | */ 1056 | 1057 | command(nameAndArgs, actionOptsOrExecDesc, execOpts) { 1058 | let desc = actionOptsOrExecDesc; 1059 | let opts = execOpts; 1060 | if (typeof desc === 'object' && desc !== null) { 1061 | opts = desc; 1062 | desc = null; 1063 | } 1064 | opts = opts || {}; 1065 | const args = nameAndArgs.split(/ +/); 1066 | const cmd = this.createCommand(args.shift()); 1067 | 1068 | if (desc) { 1069 | cmd.description(desc); 1070 | cmd._executableHandler = true; 1071 | } 1072 | if (opts.isDefault) this._defaultCommandName = cmd._name; 1073 | 1074 | cmd._outputConfiguration = this._outputConfiguration; 1075 | 1076 | cmd._hidden = !!(opts.noHelp || opts.hidden); // noHelp is deprecated old name for hidden 1077 | cmd._hasHelpOption = this._hasHelpOption; 1078 | cmd._helpFlags = this._helpFlags; 1079 | cmd._helpDescription = this._helpDescription; 1080 | cmd._helpShortFlag = this._helpShortFlag; 1081 | cmd._helpLongFlag = this._helpLongFlag; 1082 | cmd._helpCommandName = this._helpCommandName; 1083 | cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; 1084 | cmd._helpCommandDescription = this._helpCommandDescription; 1085 | cmd._helpConfiguration = this._helpConfiguration; 1086 | cmd._exitCallback = this._exitCallback; 1087 | cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; 1088 | cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue; 1089 | cmd._allowExcessArguments = this._allowExcessArguments; 1090 | cmd._enablePositionalOptions = this._enablePositionalOptions; 1091 | 1092 | cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor 1093 | this.commands.push(cmd); 1094 | cmd._parseExpectedArgs(args); 1095 | cmd.parent = this; 1096 | 1097 | if (desc) return this; 1098 | return cmd; 1099 | }; 1100 | 1101 | /** 1102 | * Factory routine to create a new unattached command. 1103 | * 1104 | * See .command() for creating an attached subcommand, which uses this routine to 1105 | * create the command. You can override createCommand to customise subcommands. 1106 | * 1107 | * @param {string} [name] 1108 | * @return {Command} new command 1109 | */ 1110 | 1111 | createCommand(name) { 1112 | return new Command(name); 1113 | }; 1114 | 1115 | /** 1116 | * You can customise the help with a subclass of Help by overriding createHelp, 1117 | * or by overriding Help properties using configureHelp(). 1118 | * 1119 | * @return {Help} 1120 | */ 1121 | 1122 | createHelp() { 1123 | return Object.assign(new Help(), this.configureHelp()); 1124 | }; 1125 | 1126 | /** 1127 | * You can customise the help by overriding Help properties using configureHelp(), 1128 | * or with a subclass of Help by overriding createHelp(). 1129 | * 1130 | * @param {Object} [configuration] - configuration options 1131 | * @return {Command|Object} `this` command for chaining, or stored configuration 1132 | */ 1133 | 1134 | configureHelp(configuration) { 1135 | if (configuration === undefined) return this._helpConfiguration; 1136 | 1137 | this._helpConfiguration = configuration; 1138 | return this; 1139 | } 1140 | 1141 | /** 1142 | * The default output goes to stdout and stderr. You can customise this for special 1143 | * applications. You can also customise the display of errors by overriding outputError. 1144 | * 1145 | * The configuration properties are all functions: 1146 | * 1147 | * // functions to change where being written, stdout and stderr 1148 | * writeOut(str) 1149 | * writeErr(str) 1150 | * // matching functions to specify width for wrapping help 1151 | * getOutHelpWidth() 1152 | * getErrHelpWidth() 1153 | * // functions based on what is being written out 1154 | * outputError(str, write) // used for displaying errors, and not used for displaying help 1155 | * 1156 | * @param {Object} [configuration] - configuration options 1157 | * @return {Command|Object} `this` command for chaining, or stored configuration 1158 | */ 1159 | 1160 | configureOutput(configuration) { 1161 | if (configuration === undefined) return this._outputConfiguration; 1162 | 1163 | Object.assign(this._outputConfiguration, configuration); 1164 | return this; 1165 | } 1166 | 1167 | /** 1168 | * Add a prepared subcommand. 1169 | * 1170 | * See .command() for creating an attached subcommand which inherits settings from its parent. 1171 | * 1172 | * @param {Command} cmd - new subcommand 1173 | * @param {Object} [opts] - configuration options 1174 | * @return {Command} `this` command for chaining 1175 | */ 1176 | 1177 | addCommand(cmd, opts) { 1178 | if (!cmd._name) throw new Error('Command passed to .addCommand() must have a name'); 1179 | 1180 | // To keep things simple, block automatic name generation for deeply nested executables. 1181 | // Fail fast and detect when adding rather than later when parsing. 1182 | function checkExplicitNames(commandArray) { 1183 | commandArray.forEach((cmd) => { 1184 | if (cmd._executableHandler && !cmd._executableFile) { 1185 | throw new Error(`Must specify executableFile for deeply nested executable: ${cmd.name()}`); 1186 | } 1187 | checkExplicitNames(cmd.commands); 1188 | }); 1189 | } 1190 | checkExplicitNames(cmd.commands); 1191 | 1192 | opts = opts || {}; 1193 | if (opts.isDefault) this._defaultCommandName = cmd._name; 1194 | if (opts.noHelp || opts.hidden) cmd._hidden = true; // modifying passed command due to existing implementation 1195 | 1196 | this.commands.push(cmd); 1197 | cmd.parent = this; 1198 | return this; 1199 | }; 1200 | 1201 | /** 1202 | * Define argument syntax for the command. 1203 | */ 1204 | 1205 | arguments(desc) { 1206 | return this._parseExpectedArgs(desc.split(/ +/)); 1207 | }; 1208 | 1209 | /** 1210 | * Override default decision whether to add implicit help command. 1211 | * 1212 | * addHelpCommand() // force on 1213 | * addHelpCommand(false); // force off 1214 | * addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details 1215 | * 1216 | * @return {Command} `this` command for chaining 1217 | */ 1218 | 1219 | addHelpCommand(enableOrNameAndArgs, description) { 1220 | if (enableOrNameAndArgs === false) { 1221 | this._addImplicitHelpCommand = false; 1222 | } else { 1223 | this._addImplicitHelpCommand = true; 1224 | if (typeof enableOrNameAndArgs === 'string') { 1225 | this._helpCommandName = enableOrNameAndArgs.split(' ')[0]; 1226 | this._helpCommandnameAndArgs = enableOrNameAndArgs; 1227 | } 1228 | this._helpCommandDescription = description || this._helpCommandDescription; 1229 | } 1230 | return this; 1231 | }; 1232 | 1233 | /** 1234 | * @return {boolean} 1235 | * @api private 1236 | */ 1237 | 1238 | _hasImplicitHelpCommand() { 1239 | if (this._addImplicitHelpCommand === undefined) { 1240 | return this.commands.length && !this._actionHandler && !this._findCommand('help'); 1241 | } 1242 | return this._addImplicitHelpCommand; 1243 | }; 1244 | 1245 | /** 1246 | * Parse expected `args`. 1247 | * 1248 | * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. 1249 | * 1250 | * @param {Array} args 1251 | * @return {Command} `this` command for chaining 1252 | * @api private 1253 | */ 1254 | 1255 | _parseExpectedArgs(args) { 1256 | if (!args.length) return; 1257 | args.forEach((arg) => { 1258 | const argDetails = { 1259 | required: false, 1260 | name: '', 1261 | variadic: false 1262 | }; 1263 | 1264 | switch (arg[0]) { 1265 | case '<': 1266 | argDetails.required = true; 1267 | argDetails.name = arg.slice(1, -1); 1268 | break; 1269 | case '[': 1270 | argDetails.name = arg.slice(1, -1); 1271 | break; 1272 | } 1273 | 1274 | if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') { 1275 | argDetails.variadic = true; 1276 | argDetails.name = argDetails.name.slice(0, -3); 1277 | } 1278 | if (argDetails.name) { 1279 | this._args.push(argDetails); 1280 | } 1281 | }); 1282 | this._args.forEach((arg, i) => { 1283 | if (arg.variadic && i < this._args.length - 1) { 1284 | throw new Error(`only the last argument can be variadic '${arg.name}'`); 1285 | } 1286 | }); 1287 | return this; 1288 | }; 1289 | 1290 | /** 1291 | * Register callback to use as replacement for calling process.exit. 1292 | * 1293 | * @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing 1294 | * @return {Command} `this` command for chaining 1295 | */ 1296 | 1297 | exitOverride(fn) { 1298 | if (fn) { 1299 | this._exitCallback = fn; 1300 | } else { 1301 | this._exitCallback = (err) => { 1302 | if (err.code !== 'commander.executeSubCommandAsync') { 1303 | throw err; 1304 | } 1305 | }; 1306 | } 1307 | return this; 1308 | }; 1309 | 1310 | /** 1311 | * Call process.exit, and _exitCallback if defined. 1312 | * 1313 | * @param {number} exitCode exit code for using with process.exit 1314 | * @param {string} code an id string representing the error 1315 | * @param {string} message human-readable description of the error 1316 | * @return never 1317 | * @api private 1318 | */ 1319 | 1320 | _exit(exitCode, code, message) { 1321 | if (this._exitCallback) { 1322 | this._exitCallback(new CommanderError(exitCode, code, message)); 1323 | // Expecting this line is not reached. 1324 | } 1325 | process.exit(exitCode); 1326 | }; 1327 | 1328 | /** 1329 | * Register callback `fn` for the command. 1330 | * 1331 | * Examples: 1332 | * 1333 | * program 1334 | * .command('help') 1335 | * .description('display verbose help') 1336 | * .action(function() { 1337 | * // output help here 1338 | * }); 1339 | * 1340 | * @param {Function} fn 1341 | * @return {Command} `this` command for chaining 1342 | */ 1343 | 1344 | action(fn) { 1345 | const listener = (args) => { 1346 | // The .action callback takes an extra parameter which is the command or options. 1347 | const expectedArgsCount = this._args.length; 1348 | const actionArgs = args.slice(0, expectedArgsCount); 1349 | if (this._storeOptionsAsProperties) { 1350 | actionArgs[expectedArgsCount] = this; // backwards compatible "options" 1351 | } else { 1352 | actionArgs[expectedArgsCount] = this.opts(); 1353 | } 1354 | actionArgs.push(this); 1355 | 1356 | const actionResult = fn.apply(this, actionArgs); 1357 | // Remember result in case it is async. Assume parseAsync getting called on root. 1358 | let rootCommand = this; 1359 | while (rootCommand.parent) { 1360 | rootCommand = rootCommand.parent; 1361 | } 1362 | rootCommand._actionResults.push(actionResult); 1363 | }; 1364 | this._actionHandler = listener; 1365 | return this; 1366 | }; 1367 | 1368 | /** 1369 | * Factory routine to create a new unattached option. 1370 | * 1371 | * See .option() for creating an attached option, which uses this routine to 1372 | * create the option. You can override createOption to return a custom option. 1373 | * 1374 | * @param {string} flags 1375 | * @param {string} [description] 1376 | * @return {Option} new option 1377 | */ 1378 | 1379 | createOption(flags, description) { 1380 | return new Option(flags, description); 1381 | }; 1382 | 1383 | /** 1384 | * Add an option. 1385 | * 1386 | * @param {Option} option 1387 | * @return {Command} `this` command for chaining 1388 | */ 1389 | addOption(option) { 1390 | const oname = option.name(); 1391 | const name = option.attributeName(); 1392 | 1393 | let defaultValue = option.defaultValue; 1394 | 1395 | // preassign default value for --no-*, [optional], , or plain flag if boolean value 1396 | if (option.negate || option.optional || option.required || typeof defaultValue === 'boolean') { 1397 | // when --no-foo we make sure default is true, unless a --foo option is already defined 1398 | if (option.negate) { 1399 | const positiveLongFlag = option.long.replace(/^--no-/, '--'); 1400 | defaultValue = this._findOption(positiveLongFlag) ? this._getOptionValue(name) : true; 1401 | } 1402 | // preassign only if we have a default 1403 | if (defaultValue !== undefined) { 1404 | this._setOptionValue(name, defaultValue); 1405 | } 1406 | } 1407 | 1408 | // register the option 1409 | this.options.push(option); 1410 | 1411 | // when it's passed assign the value 1412 | // and conditionally invoke the callback 1413 | this.on('option:' + oname, (val) => { 1414 | const oldValue = this._getOptionValue(name); 1415 | 1416 | // custom processing 1417 | if (val !== null && option.parseArg) { 1418 | try { 1419 | val = option.parseArg(val, oldValue === undefined ? defaultValue : oldValue); 1420 | } catch (err) { 1421 | if (err.code === 'commander.invalidOptionArgument') { 1422 | const message = `error: option '${option.flags}' argument '${val}' is invalid. ${err.message}`; 1423 | this._displayError(err.exitCode, err.code, message); 1424 | } 1425 | throw err; 1426 | } 1427 | } else if (val !== null && option.variadic) { 1428 | val = option._concatValue(val, oldValue); 1429 | } 1430 | 1431 | // unassigned or boolean value 1432 | if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') { 1433 | // if no value, negate false, and we have a default, then use it! 1434 | if (val == null) { 1435 | this._setOptionValue(name, option.negate 1436 | ? false 1437 | : defaultValue || true); 1438 | } else { 1439 | this._setOptionValue(name, val); 1440 | } 1441 | } else if (val !== null) { 1442 | // reassign 1443 | this._setOptionValue(name, option.negate ? false : val); 1444 | } 1445 | }); 1446 | 1447 | return this; 1448 | } 1449 | 1450 | /** 1451 | * Internal implementation shared by .option() and .requiredOption() 1452 | * 1453 | * @api private 1454 | */ 1455 | _optionEx(config, flags, description, fn, defaultValue) { 1456 | const option = this.createOption(flags, description); 1457 | option.makeOptionMandatory(!!config.mandatory); 1458 | if (typeof fn === 'function') { 1459 | option.default(defaultValue).argParser(fn); 1460 | } else if (fn instanceof RegExp) { 1461 | // deprecated 1462 | const regex = fn; 1463 | fn = (val, def) => { 1464 | const m = regex.exec(val); 1465 | return m ? m[0] : def; 1466 | }; 1467 | option.default(defaultValue).argParser(fn); 1468 | } else { 1469 | option.default(fn); 1470 | } 1471 | 1472 | return this.addOption(option); 1473 | } 1474 | 1475 | /** 1476 | * Define option with `flags`, `description` and optional 1477 | * coercion `fn`. 1478 | * 1479 | * The `flags` string contains the short and/or long flags, 1480 | * separated by comma, a pipe or space. The following are all valid 1481 | * all will output this way when `--help` is used. 1482 | * 1483 | * "-p, --pepper" 1484 | * "-p|--pepper" 1485 | * "-p --pepper" 1486 | * 1487 | * Examples: 1488 | * 1489 | * // simple boolean defaulting to undefined 1490 | * program.option('-p, --pepper', 'add pepper'); 1491 | * 1492 | * program.pepper 1493 | * // => undefined 1494 | * 1495 | * --pepper 1496 | * program.pepper 1497 | * // => true 1498 | * 1499 | * // simple boolean defaulting to true (unless non-negated option is also defined) 1500 | * program.option('-C, --no-cheese', 'remove cheese'); 1501 | * 1502 | * program.cheese 1503 | * // => true 1504 | * 1505 | * --no-cheese 1506 | * program.cheese 1507 | * // => false 1508 | * 1509 | * // required argument 1510 | * program.option('-C, --chdir ', 'change the working directory'); 1511 | * 1512 | * --chdir /tmp 1513 | * program.chdir 1514 | * // => "/tmp" 1515 | * 1516 | * // optional argument 1517 | * program.option('-c, --cheese [type]', 'add cheese [marble]'); 1518 | * 1519 | * @param {string} flags 1520 | * @param {string} [description] 1521 | * @param {Function|*} [fn] - custom option processing function or default value 1522 | * @param {*} [defaultValue] 1523 | * @return {Command} `this` command for chaining 1524 | */ 1525 | 1526 | option(flags, description, fn, defaultValue) { 1527 | return this._optionEx({}, flags, description, fn, defaultValue); 1528 | }; 1529 | 1530 | /** 1531 | * Add a required option which must have a value after parsing. This usually means 1532 | * the option must be specified on the command line. (Otherwise the same as .option().) 1533 | * 1534 | * The `flags` string contains the short and/or long flags, separated by comma, a pipe or space. 1535 | * 1536 | * @param {string} flags 1537 | * @param {string} [description] 1538 | * @param {Function|*} [fn] - custom option processing function or default value 1539 | * @param {*} [defaultValue] 1540 | * @return {Command} `this` command for chaining 1541 | */ 1542 | 1543 | requiredOption(flags, description, fn, defaultValue) { 1544 | return this._optionEx({ mandatory: true }, flags, description, fn, defaultValue); 1545 | }; 1546 | 1547 | /** 1548 | * Alter parsing of short flags with optional values. 1549 | * 1550 | * Examples: 1551 | * 1552 | * // for `.option('-f,--flag [value]'): 1553 | * .combineFlagAndOptionalValue(true) // `-f80` is treated like `--flag=80`, this is the default behaviour 1554 | * .combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b` 1555 | * 1556 | * @param {Boolean} [combine=true] - if `true` or omitted, an optional value can be specified directly after the flag. 1557 | */ 1558 | combineFlagAndOptionalValue(combine = true) { 1559 | this._combineFlagAndOptionalValue = !!combine; 1560 | return this; 1561 | }; 1562 | 1563 | /** 1564 | * Allow unknown options on the command line. 1565 | * 1566 | * @param {Boolean} [allowUnknown=true] - if `true` or omitted, no error will be thrown 1567 | * for unknown options. 1568 | */ 1569 | allowUnknownOption(allowUnknown = true) { 1570 | this._allowUnknownOption = !!allowUnknown; 1571 | return this; 1572 | }; 1573 | 1574 | /** 1575 | * Allow excess command-arguments on the command line. Pass false to make excess arguments an error. 1576 | * 1577 | * @param {Boolean} [allowExcess=true] - if `true` or omitted, no error will be thrown 1578 | * for excess arguments. 1579 | */ 1580 | allowExcessArguments(allowExcess = true) { 1581 | this._allowExcessArguments = !!allowExcess; 1582 | return this; 1583 | }; 1584 | 1585 | /** 1586 | * Enable positional options. Positional means global options are specified before subcommands which lets 1587 | * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. 1588 | * The default behaviour is non-positional and global options may appear anywhere on the command line. 1589 | * 1590 | * @param {Boolean} [positional=true] 1591 | */ 1592 | enablePositionalOptions(positional = true) { 1593 | this._enablePositionalOptions = !!positional; 1594 | return this; 1595 | }; 1596 | 1597 | /** 1598 | * Pass through options that come after command-arguments rather than treat them as command-options, 1599 | * so actual command-options come before command-arguments. Turning this on for a subcommand requires 1600 | * positional options to have been enabled on the program (parent commands). 1601 | * The default behaviour is non-positional and options may appear before or after command-arguments. 1602 | * 1603 | * @param {Boolean} [passThrough=true] 1604 | * for unknown options. 1605 | */ 1606 | passThroughOptions(passThrough = true) { 1607 | this._passThroughOptions = !!passThrough; 1608 | if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) { 1609 | throw new Error('passThroughOptions can not be used without turning on enablePositionalOptions for parent command(s)'); 1610 | } 1611 | return this; 1612 | }; 1613 | 1614 | /** 1615 | * Whether to store option values as properties on command object, 1616 | * or store separately (specify false). In both cases the option values can be accessed using .opts(). 1617 | * 1618 | * @param {boolean} [storeAsProperties=true] 1619 | * @return {Command} `this` command for chaining 1620 | */ 1621 | 1622 | storeOptionsAsProperties(storeAsProperties = true) { 1623 | this._storeOptionsAsProperties = !!storeAsProperties; 1624 | if (this.options.length) { 1625 | throw new Error('call .storeOptionsAsProperties() before adding options'); 1626 | } 1627 | return this; 1628 | }; 1629 | 1630 | /** 1631 | * Store option value 1632 | * 1633 | * @param {string} key 1634 | * @param {Object} value 1635 | * @api private 1636 | */ 1637 | 1638 | _setOptionValue(key, value) { 1639 | if (this._storeOptionsAsProperties) { 1640 | this[key] = value; 1641 | } else { 1642 | this._optionValues[key] = value; 1643 | } 1644 | }; 1645 | 1646 | /** 1647 | * Retrieve option value 1648 | * 1649 | * @param {string} key 1650 | * @return {Object} value 1651 | * @api private 1652 | */ 1653 | 1654 | _getOptionValue(key) { 1655 | if (this._storeOptionsAsProperties) { 1656 | return this[key]; 1657 | } 1658 | return this._optionValues[key]; 1659 | }; 1660 | 1661 | /** 1662 | * Parse `argv`, setting options and invoking commands when defined. 1663 | * 1664 | * The default expectation is that the arguments are from node and have the application as argv[0] 1665 | * and the script being run in argv[1], with user parameters after that. 1666 | * 1667 | * Examples: 1668 | * 1669 | * program.parse(process.argv); 1670 | * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions 1671 | * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] 1672 | * 1673 | * @param {string[]} [argv] - optional, defaults to process.argv 1674 | * @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron 1675 | * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron' 1676 | * @return {Command} `this` command for chaining 1677 | */ 1678 | 1679 | parse(argv, parseOptions) { 1680 | if (argv !== undefined && !Array.isArray(argv)) { 1681 | throw new Error('first parameter to parse must be array or undefined'); 1682 | } 1683 | parseOptions = parseOptions || {}; 1684 | 1685 | // Default to using process.argv 1686 | if (argv === undefined) { 1687 | argv = process.argv; 1688 | // @ts-ignore: unknown property 1689 | if (process.versions && process.versions.electron) { 1690 | parseOptions.from = 'electron'; 1691 | } 1692 | } 1693 | this.rawArgs = argv.slice(); 1694 | 1695 | // make it a little easier for callers by supporting various argv conventions 1696 | let userArgs; 1697 | switch (parseOptions.from) { 1698 | case undefined: 1699 | case 'node': 1700 | this._scriptPath = argv[1]; 1701 | userArgs = argv.slice(2); 1702 | break; 1703 | case 'electron': 1704 | // @ts-ignore: unknown property 1705 | if (process.defaultApp) { 1706 | this._scriptPath = argv[1]; 1707 | userArgs = argv.slice(2); 1708 | } else { 1709 | userArgs = argv.slice(1); 1710 | } 1711 | break; 1712 | case 'user': 1713 | userArgs = argv.slice(0); 1714 | break; 1715 | default: 1716 | throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`); 1717 | } 1718 | if (!this._scriptPath && commonjsRequire.main) { 1719 | this._scriptPath = commonjsRequire.main.filename; 1720 | } 1721 | 1722 | // Guess name, used in usage in help. 1723 | this._name = this._name || (this._scriptPath && path__default['default'].basename(this._scriptPath, path__default['default'].extname(this._scriptPath))); 1724 | 1725 | // Let's go! 1726 | this._parseCommand([], userArgs); 1727 | 1728 | return this; 1729 | }; 1730 | 1731 | /** 1732 | * Parse `argv`, setting options and invoking commands when defined. 1733 | * 1734 | * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. 1735 | * 1736 | * The default expectation is that the arguments are from node and have the application as argv[0] 1737 | * and the script being run in argv[1], with user parameters after that. 1738 | * 1739 | * Examples: 1740 | * 1741 | * program.parseAsync(process.argv); 1742 | * program.parseAsync(); // implicitly use process.argv and auto-detect node vs electron conventions 1743 | * program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] 1744 | * 1745 | * @param {string[]} [argv] 1746 | * @param {Object} [parseOptions] 1747 | * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' 1748 | * @return {Promise} 1749 | */ 1750 | 1751 | parseAsync(argv, parseOptions) { 1752 | this.parse(argv, parseOptions); 1753 | return Promise.all(this._actionResults).then(() => this); 1754 | }; 1755 | 1756 | /** 1757 | * Execute a sub-command executable. 1758 | * 1759 | * @api private 1760 | */ 1761 | 1762 | _executeSubCommand(subcommand, args) { 1763 | args = args.slice(); 1764 | let launchWithNode = false; // Use node for source targets so do not need to get permissions correct, and on Windows. 1765 | const sourceExt = ['.js', '.ts', '.tsx', '.mjs', '.cjs']; 1766 | 1767 | // Not checking for help first. Unlikely to have mandatory and executable, and can't robustly test for help flags in external command. 1768 | this._checkForMissingMandatoryOptions(); 1769 | 1770 | // Want the entry script as the reference for command name and directory for searching for other files. 1771 | let scriptPath = this._scriptPath; 1772 | // Fallback in case not set, due to how Command created or called. 1773 | if (!scriptPath && commonjsRequire.main) { 1774 | scriptPath = commonjsRequire.main.filename; 1775 | } 1776 | 1777 | let baseDir; 1778 | try { 1779 | const resolvedLink = fs__default['default'].realpathSync(scriptPath); 1780 | baseDir = path__default['default'].dirname(resolvedLink); 1781 | } catch (e) { 1782 | baseDir = '.'; // dummy, probably not going to find executable! 1783 | } 1784 | 1785 | // name of the subcommand, like `pm-install` 1786 | let bin = path__default['default'].basename(scriptPath, path__default['default'].extname(scriptPath)) + '-' + subcommand._name; 1787 | if (subcommand._executableFile) { 1788 | bin = subcommand._executableFile; 1789 | } 1790 | 1791 | const localBin = path__default['default'].join(baseDir, bin); 1792 | if (fs__default['default'].existsSync(localBin)) { 1793 | // prefer local `./` to bin in the $PATH 1794 | bin = localBin; 1795 | } else { 1796 | // Look for source files. 1797 | sourceExt.forEach((ext) => { 1798 | if (fs__default['default'].existsSync(`${localBin}${ext}`)) { 1799 | bin = `${localBin}${ext}`; 1800 | } 1801 | }); 1802 | } 1803 | launchWithNode = sourceExt.includes(path__default['default'].extname(bin)); 1804 | 1805 | let proc; 1806 | if (process.platform !== 'win32') { 1807 | if (launchWithNode) { 1808 | args.unshift(bin); 1809 | // add executable arguments to spawn 1810 | args = incrementNodeInspectorPort(process.execArgv).concat(args); 1811 | 1812 | proc = child_process__default['default'].spawn(process.argv[0], args, { stdio: 'inherit' }); 1813 | } else { 1814 | proc = child_process__default['default'].spawn(bin, args, { stdio: 'inherit' }); 1815 | } 1816 | } else { 1817 | args.unshift(bin); 1818 | // add executable arguments to spawn 1819 | args = incrementNodeInspectorPort(process.execArgv).concat(args); 1820 | proc = child_process__default['default'].spawn(process.execPath, args, { stdio: 'inherit' }); 1821 | } 1822 | 1823 | const signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP']; 1824 | signals.forEach((signal) => { 1825 | // @ts-ignore 1826 | process.on(signal, () => { 1827 | if (proc.killed === false && proc.exitCode === null) { 1828 | proc.kill(signal); 1829 | } 1830 | }); 1831 | }); 1832 | 1833 | // By default terminate process when spawned process terminates. 1834 | // Suppressing the exit if exitCallback defined is a bit messy and of limited use, but does allow process to stay running! 1835 | const exitCallback = this._exitCallback; 1836 | if (!exitCallback) { 1837 | proc.on('close', process.exit.bind(process)); 1838 | } else { 1839 | proc.on('close', () => { 1840 | exitCallback(new CommanderError(process.exitCode || 0, 'commander.executeSubCommandAsync', '(close)')); 1841 | }); 1842 | } 1843 | proc.on('error', (err) => { 1844 | // @ts-ignore 1845 | if (err.code === 'ENOENT') { 1846 | const executableMissing = `'${bin}' does not exist 1847 | - if '${subcommand._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead 1848 | - if the default executable name is not suitable, use the executableFile option to supply a custom name`; 1849 | throw new Error(executableMissing); 1850 | // @ts-ignore 1851 | } else if (err.code === 'EACCES') { 1852 | throw new Error(`'${bin}' not executable`); 1853 | } 1854 | if (!exitCallback) { 1855 | process.exit(1); 1856 | } else { 1857 | const wrappedError = new CommanderError(1, 'commander.executeSubCommandAsync', '(error)'); 1858 | wrappedError.nestedError = err; 1859 | exitCallback(wrappedError); 1860 | } 1861 | }); 1862 | 1863 | // Store the reference to the child process 1864 | this.runningCommand = proc; 1865 | }; 1866 | 1867 | /** 1868 | * @api private 1869 | */ 1870 | _dispatchSubcommand(commandName, operands, unknown) { 1871 | const subCommand = this._findCommand(commandName); 1872 | if (!subCommand) this.help({ error: true }); 1873 | 1874 | if (subCommand._executableHandler) { 1875 | this._executeSubCommand(subCommand, operands.concat(unknown)); 1876 | } else { 1877 | subCommand._parseCommand(operands, unknown); 1878 | } 1879 | }; 1880 | 1881 | /** 1882 | * Process arguments in context of this command. 1883 | * 1884 | * @api private 1885 | */ 1886 | 1887 | _parseCommand(operands, unknown) { 1888 | const parsed = this.parseOptions(unknown); 1889 | operands = operands.concat(parsed.operands); 1890 | unknown = parsed.unknown; 1891 | this.args = operands.concat(unknown); 1892 | 1893 | if (operands && this._findCommand(operands[0])) { 1894 | this._dispatchSubcommand(operands[0], operands.slice(1), unknown); 1895 | } else if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { 1896 | if (operands.length === 1) { 1897 | this.help(); 1898 | } else { 1899 | this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]); 1900 | } 1901 | } else if (this._defaultCommandName) { 1902 | outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command 1903 | this._dispatchSubcommand(this._defaultCommandName, operands, unknown); 1904 | } else { 1905 | if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { 1906 | // probably missing subcommand and no handler, user needs help 1907 | this.help({ error: true }); 1908 | } 1909 | 1910 | outputHelpIfRequested(this, parsed.unknown); 1911 | this._checkForMissingMandatoryOptions(); 1912 | 1913 | // We do not always call this check to avoid masking a "better" error, like unknown command. 1914 | const checkForUnknownOptions = () => { 1915 | if (parsed.unknown.length > 0) { 1916 | this.unknownOption(parsed.unknown[0]); 1917 | } 1918 | }; 1919 | 1920 | const commandEvent = `command:${this.name()}`; 1921 | if (this._actionHandler) { 1922 | checkForUnknownOptions(); 1923 | // Check expected arguments and collect variadic together. 1924 | const args = this.args.slice(); 1925 | this._args.forEach((arg, i) => { 1926 | if (arg.required && args[i] == null) { 1927 | this.missingArgument(arg.name); 1928 | } else if (arg.variadic) { 1929 | args[i] = args.splice(i); 1930 | args.length = Math.min(i + 1, args.length); 1931 | } 1932 | }); 1933 | if (args.length > this._args.length) { 1934 | this._excessArguments(args); 1935 | } 1936 | 1937 | this._actionHandler(args); 1938 | if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy 1939 | } else if (this.parent && this.parent.listenerCount(commandEvent)) { 1940 | checkForUnknownOptions(); 1941 | this.parent.emit(commandEvent, operands, unknown); // legacy 1942 | } else if (operands.length) { 1943 | if (this._findCommand('*')) { // legacy default command 1944 | this._dispatchSubcommand('*', operands, unknown); 1945 | } else if (this.listenerCount('command:*')) { 1946 | // skip option check, emit event for possible misspelling suggestion 1947 | this.emit('command:*', operands, unknown); 1948 | } else if (this.commands.length) { 1949 | this.unknownCommand(); 1950 | } else { 1951 | checkForUnknownOptions(); 1952 | } 1953 | } else if (this.commands.length) { 1954 | // This command has subcommands and nothing hooked up at this level, so display help. 1955 | this.help({ error: true }); 1956 | } else { 1957 | checkForUnknownOptions(); 1958 | // fall through for caller to handle after calling .parse() 1959 | } 1960 | } 1961 | }; 1962 | 1963 | /** 1964 | * Find matching command. 1965 | * 1966 | * @api private 1967 | */ 1968 | _findCommand(name) { 1969 | if (!name) return undefined; 1970 | return this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name)); 1971 | }; 1972 | 1973 | /** 1974 | * Return an option matching `arg` if any. 1975 | * 1976 | * @param {string} arg 1977 | * @return {Option} 1978 | * @api private 1979 | */ 1980 | 1981 | _findOption(arg) { 1982 | return this.options.find(option => option.is(arg)); 1983 | }; 1984 | 1985 | /** 1986 | * Display an error message if a mandatory option does not have a value. 1987 | * Lazy calling after checking for help flags from leaf subcommand. 1988 | * 1989 | * @api private 1990 | */ 1991 | 1992 | _checkForMissingMandatoryOptions() { 1993 | // Walk up hierarchy so can call in subcommand after checking for displaying help. 1994 | for (let cmd = this; cmd; cmd = cmd.parent) { 1995 | cmd.options.forEach((anOption) => { 1996 | if (anOption.mandatory && (cmd._getOptionValue(anOption.attributeName()) === undefined)) { 1997 | cmd.missingMandatoryOptionValue(anOption); 1998 | } 1999 | }); 2000 | } 2001 | }; 2002 | 2003 | /** 2004 | * Parse options from `argv` removing known options, 2005 | * and return argv split into operands and unknown arguments. 2006 | * 2007 | * Examples: 2008 | * 2009 | * argv => operands, unknown 2010 | * --known kkk op => [op], [] 2011 | * op --known kkk => [op], [] 2012 | * sub --unknown uuu op => [sub], [--unknown uuu op] 2013 | * sub -- --unknown uuu op => [sub --unknown uuu op], [] 2014 | * 2015 | * @param {String[]} argv 2016 | * @return {{operands: String[], unknown: String[]}} 2017 | */ 2018 | 2019 | parseOptions(argv) { 2020 | const operands = []; // operands, not options or values 2021 | const unknown = []; // first unknown option and remaining unknown args 2022 | let dest = operands; 2023 | const args = argv.slice(); 2024 | 2025 | function maybeOption(arg) { 2026 | return arg.length > 1 && arg[0] === '-'; 2027 | } 2028 | 2029 | // parse options 2030 | let activeVariadicOption = null; 2031 | while (args.length) { 2032 | const arg = args.shift(); 2033 | 2034 | // literal 2035 | if (arg === '--') { 2036 | if (dest === unknown) dest.push(arg); 2037 | dest.push(...args); 2038 | break; 2039 | } 2040 | 2041 | if (activeVariadicOption && !maybeOption(arg)) { 2042 | this.emit(`option:${activeVariadicOption.name()}`, arg); 2043 | continue; 2044 | } 2045 | activeVariadicOption = null; 2046 | 2047 | if (maybeOption(arg)) { 2048 | const option = this._findOption(arg); 2049 | // recognised option, call listener to assign value with possible custom processing 2050 | if (option) { 2051 | if (option.required) { 2052 | const value = args.shift(); 2053 | if (value === undefined) this.optionMissingArgument(option); 2054 | this.emit(`option:${option.name()}`, value); 2055 | } else if (option.optional) { 2056 | let value = null; 2057 | // historical behaviour is optional value is following arg unless an option 2058 | if (args.length > 0 && !maybeOption(args[0])) { 2059 | value = args.shift(); 2060 | } 2061 | this.emit(`option:${option.name()}`, value); 2062 | } else { // boolean flag 2063 | this.emit(`option:${option.name()}`); 2064 | } 2065 | activeVariadicOption = option.variadic ? option : null; 2066 | continue; 2067 | } 2068 | } 2069 | 2070 | // Look for combo options following single dash, eat first one if known. 2071 | if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { 2072 | const option = this._findOption(`-${arg[1]}`); 2073 | if (option) { 2074 | if (option.required || (option.optional && this._combineFlagAndOptionalValue)) { 2075 | // option with value following in same argument 2076 | this.emit(`option:${option.name()}`, arg.slice(2)); 2077 | } else { 2078 | // boolean option, emit and put back remainder of arg for further processing 2079 | this.emit(`option:${option.name()}`); 2080 | args.unshift(`-${arg.slice(2)}`); 2081 | } 2082 | continue; 2083 | } 2084 | } 2085 | 2086 | // Look for known long flag with value, like --foo=bar 2087 | if (/^--[^=]+=/.test(arg)) { 2088 | const index = arg.indexOf('='); 2089 | const option = this._findOption(arg.slice(0, index)); 2090 | if (option && (option.required || option.optional)) { 2091 | this.emit(`option:${option.name()}`, arg.slice(index + 1)); 2092 | continue; 2093 | } 2094 | } 2095 | 2096 | // Not a recognised option by this command. 2097 | // Might be a command-argument, or subcommand option, or unknown option, or help command or option. 2098 | 2099 | // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands. 2100 | if (maybeOption(arg)) { 2101 | dest = unknown; 2102 | } 2103 | 2104 | // If using positionalOptions, stop processing our options at subcommand. 2105 | if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { 2106 | if (this._findCommand(arg)) { 2107 | operands.push(arg); 2108 | if (args.length > 0) unknown.push(...args); 2109 | break; 2110 | } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) { 2111 | operands.push(arg); 2112 | if (args.length > 0) operands.push(...args); 2113 | break; 2114 | } else if (this._defaultCommandName) { 2115 | unknown.push(arg); 2116 | if (args.length > 0) unknown.push(...args); 2117 | break; 2118 | } 2119 | } 2120 | 2121 | // If using passThroughOptions, stop processing options at first command-argument. 2122 | if (this._passThroughOptions) { 2123 | dest.push(arg); 2124 | if (args.length > 0) dest.push(...args); 2125 | break; 2126 | } 2127 | 2128 | // add arg 2129 | dest.push(arg); 2130 | } 2131 | 2132 | return { operands, unknown }; 2133 | }; 2134 | 2135 | /** 2136 | * Return an object containing options as key-value pairs 2137 | * 2138 | * @return {Object} 2139 | */ 2140 | opts() { 2141 | if (this._storeOptionsAsProperties) { 2142 | // Preserve original behaviour so backwards compatible when still using properties 2143 | const result = {}; 2144 | const len = this.options.length; 2145 | 2146 | for (let i = 0; i < len; i++) { 2147 | const key = this.options[i].attributeName(); 2148 | result[key] = key === this._versionOptionName ? this._version : this[key]; 2149 | } 2150 | return result; 2151 | } 2152 | 2153 | return this._optionValues; 2154 | }; 2155 | 2156 | /** 2157 | * Internal bottleneck for handling of parsing errors. 2158 | * 2159 | * @api private 2160 | */ 2161 | _displayError(exitCode, code, message) { 2162 | this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr); 2163 | this._exit(exitCode, code, message); 2164 | } 2165 | 2166 | /** 2167 | * Argument `name` is missing. 2168 | * 2169 | * @param {string} name 2170 | * @api private 2171 | */ 2172 | 2173 | missingArgument(name) { 2174 | const message = `error: missing required argument '${name}'`; 2175 | this._displayError(1, 'commander.missingArgument', message); 2176 | }; 2177 | 2178 | /** 2179 | * `Option` is missing an argument. 2180 | * 2181 | * @param {Option} option 2182 | * @api private 2183 | */ 2184 | 2185 | optionMissingArgument(option) { 2186 | const message = `error: option '${option.flags}' argument missing`; 2187 | this._displayError(1, 'commander.optionMissingArgument', message); 2188 | }; 2189 | 2190 | /** 2191 | * `Option` does not have a value, and is a mandatory option. 2192 | * 2193 | * @param {Option} option 2194 | * @api private 2195 | */ 2196 | 2197 | missingMandatoryOptionValue(option) { 2198 | const message = `error: required option '${option.flags}' not specified`; 2199 | this._displayError(1, 'commander.missingMandatoryOptionValue', message); 2200 | }; 2201 | 2202 | /** 2203 | * Unknown option `flag`. 2204 | * 2205 | * @param {string} flag 2206 | * @api private 2207 | */ 2208 | 2209 | unknownOption(flag) { 2210 | if (this._allowUnknownOption) return; 2211 | const message = `error: unknown option '${flag}'`; 2212 | this._displayError(1, 'commander.unknownOption', message); 2213 | }; 2214 | 2215 | /** 2216 | * Excess arguments, more than expected. 2217 | * 2218 | * @param {string[]} receivedArgs 2219 | * @api private 2220 | */ 2221 | 2222 | _excessArguments(receivedArgs) { 2223 | if (this._allowExcessArguments) return; 2224 | 2225 | const expected = this._args.length; 2226 | const s = (expected === 1) ? '' : 's'; 2227 | const forSubcommand = this.parent ? ` for '${this.name()}'` : ''; 2228 | const message = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`; 2229 | this._displayError(1, 'commander.excessArguments', message); 2230 | }; 2231 | 2232 | /** 2233 | * Unknown command. 2234 | * 2235 | * @api private 2236 | */ 2237 | 2238 | unknownCommand() { 2239 | const partCommands = [this.name()]; 2240 | for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) { 2241 | partCommands.unshift(parentCmd.name()); 2242 | } 2243 | const fullCommand = partCommands.join(' '); 2244 | const message = `error: unknown command '${this.args[0]}'.` + 2245 | (this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : ''); 2246 | this._displayError(1, 'commander.unknownCommand', message); 2247 | }; 2248 | 2249 | /** 2250 | * Set the program version to `str`. 2251 | * 2252 | * This method auto-registers the "-V, --version" flag 2253 | * which will print the version number when passed. 2254 | * 2255 | * You can optionally supply the flags and description to override the defaults. 2256 | * 2257 | * @param {string} str 2258 | * @param {string} [flags] 2259 | * @param {string} [description] 2260 | * @return {this | string} `this` command for chaining, or version string if no arguments 2261 | */ 2262 | 2263 | version(str, flags, description) { 2264 | if (str === undefined) return this._version; 2265 | this._version = str; 2266 | flags = flags || '-V, --version'; 2267 | description = description || 'output the version number'; 2268 | const versionOption = this.createOption(flags, description); 2269 | this._versionOptionName = versionOption.attributeName(); 2270 | this.options.push(versionOption); 2271 | this.on('option:' + versionOption.name(), () => { 2272 | this._outputConfiguration.writeOut(`${str}\n`); 2273 | this._exit(0, 'commander.version', str); 2274 | }); 2275 | return this; 2276 | }; 2277 | 2278 | /** 2279 | * Set the description to `str`. 2280 | * 2281 | * @param {string} [str] 2282 | * @param {Object} [argsDescription] 2283 | * @return {string|Command} 2284 | */ 2285 | description(str, argsDescription) { 2286 | if (str === undefined && argsDescription === undefined) return this._description; 2287 | this._description = str; 2288 | this._argsDescription = argsDescription; 2289 | return this; 2290 | }; 2291 | 2292 | /** 2293 | * Set an alias for the command. 2294 | * 2295 | * You may call more than once to add multiple aliases. Only the first alias is shown in the auto-generated help. 2296 | * 2297 | * @param {string} [alias] 2298 | * @return {string|Command} 2299 | */ 2300 | 2301 | alias(alias) { 2302 | if (alias === undefined) return this._aliases[0]; // just return first, for backwards compatibility 2303 | 2304 | let command = this; 2305 | if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) { 2306 | // assume adding alias for last added executable subcommand, rather than this 2307 | command = this.commands[this.commands.length - 1]; 2308 | } 2309 | 2310 | if (alias === command._name) throw new Error('Command alias can\'t be the same as its name'); 2311 | 2312 | command._aliases.push(alias); 2313 | return this; 2314 | }; 2315 | 2316 | /** 2317 | * Set aliases for the command. 2318 | * 2319 | * Only the first alias is shown in the auto-generated help. 2320 | * 2321 | * @param {string[]} [aliases] 2322 | * @return {string[]|Command} 2323 | */ 2324 | 2325 | aliases(aliases) { 2326 | // Getter for the array of aliases is the main reason for having aliases() in addition to alias(). 2327 | if (aliases === undefined) return this._aliases; 2328 | 2329 | aliases.forEach((alias) => this.alias(alias)); 2330 | return this; 2331 | }; 2332 | 2333 | /** 2334 | * Set / get the command usage `str`. 2335 | * 2336 | * @param {string} [str] 2337 | * @return {String|Command} 2338 | */ 2339 | 2340 | usage(str) { 2341 | if (str === undefined) { 2342 | if (this._usage) return this._usage; 2343 | 2344 | const args = this._args.map((arg) => { 2345 | return humanReadableArgName(arg); 2346 | }); 2347 | return [].concat( 2348 | (this.options.length || this._hasHelpOption ? '[options]' : []), 2349 | (this.commands.length ? '[command]' : []), 2350 | (this._args.length ? args : []) 2351 | ).join(' '); 2352 | } 2353 | 2354 | this._usage = str; 2355 | return this; 2356 | }; 2357 | 2358 | /** 2359 | * Get or set the name of the command 2360 | * 2361 | * @param {string} [str] 2362 | * @return {string|Command} 2363 | */ 2364 | 2365 | name(str) { 2366 | if (str === undefined) return this._name; 2367 | this._name = str; 2368 | return this; 2369 | }; 2370 | 2371 | /** 2372 | * Return program help documentation. 2373 | * 2374 | * @param {{ error: boolean }} [contextOptions] - pass {error:true} to wrap for stderr instead of stdout 2375 | * @return {string} 2376 | */ 2377 | 2378 | helpInformation(contextOptions) { 2379 | const helper = this.createHelp(); 2380 | if (helper.helpWidth === undefined) { 2381 | helper.helpWidth = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrHelpWidth() : this._outputConfiguration.getOutHelpWidth(); 2382 | } 2383 | return helper.formatHelp(this, helper); 2384 | }; 2385 | 2386 | /** 2387 | * @api private 2388 | */ 2389 | 2390 | _getHelpContext(contextOptions) { 2391 | contextOptions = contextOptions || {}; 2392 | const context = { error: !!contextOptions.error }; 2393 | let write; 2394 | if (context.error) { 2395 | write = (arg) => this._outputConfiguration.writeErr(arg); 2396 | } else { 2397 | write = (arg) => this._outputConfiguration.writeOut(arg); 2398 | } 2399 | context.write = contextOptions.write || write; 2400 | context.command = this; 2401 | return context; 2402 | } 2403 | 2404 | /** 2405 | * Output help information for this command. 2406 | * 2407 | * Outputs built-in help, and custom text added using `.addHelpText()`. 2408 | * 2409 | * @param {{ error: boolean } | Function} [contextOptions] - pass {error:true} to write to stderr instead of stdout 2410 | */ 2411 | 2412 | outputHelp(contextOptions) { 2413 | let deprecatedCallback; 2414 | if (typeof contextOptions === 'function') { 2415 | deprecatedCallback = contextOptions; 2416 | contextOptions = undefined; 2417 | } 2418 | const context = this._getHelpContext(contextOptions); 2419 | 2420 | const groupListeners = []; 2421 | let command = this; 2422 | while (command) { 2423 | groupListeners.push(command); // ordered from current command to root 2424 | command = command.parent; 2425 | } 2426 | 2427 | groupListeners.slice().reverse().forEach(command => command.emit('beforeAllHelp', context)); 2428 | this.emit('beforeHelp', context); 2429 | 2430 | let helpInformation = this.helpInformation(context); 2431 | if (deprecatedCallback) { 2432 | helpInformation = deprecatedCallback(helpInformation); 2433 | if (typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation)) { 2434 | throw new Error('outputHelp callback must return a string or a Buffer'); 2435 | } 2436 | } 2437 | context.write(helpInformation); 2438 | 2439 | this.emit(this._helpLongFlag); // deprecated 2440 | this.emit('afterHelp', context); 2441 | groupListeners.forEach(command => command.emit('afterAllHelp', context)); 2442 | }; 2443 | 2444 | /** 2445 | * You can pass in flags and a description to override the help 2446 | * flags and help description for your command. Pass in false to 2447 | * disable the built-in help option. 2448 | * 2449 | * @param {string | boolean} [flags] 2450 | * @param {string} [description] 2451 | * @return {Command} `this` command for chaining 2452 | */ 2453 | 2454 | helpOption(flags, description) { 2455 | if (typeof flags === 'boolean') { 2456 | this._hasHelpOption = flags; 2457 | return this; 2458 | } 2459 | this._helpFlags = flags || this._helpFlags; 2460 | this._helpDescription = description || this._helpDescription; 2461 | 2462 | const helpFlags = _parseOptionFlags(this._helpFlags); 2463 | this._helpShortFlag = helpFlags.shortFlag; 2464 | this._helpLongFlag = helpFlags.longFlag; 2465 | 2466 | return this; 2467 | }; 2468 | 2469 | /** 2470 | * Output help information and exit. 2471 | * 2472 | * Outputs built-in help, and custom text added using `.addHelpText()`. 2473 | * 2474 | * @param {{ error: boolean }} [contextOptions] - pass {error:true} to write to stderr instead of stdout 2475 | */ 2476 | 2477 | help(contextOptions) { 2478 | this.outputHelp(contextOptions); 2479 | let exitCode = process.exitCode || 0; 2480 | if (exitCode === 0 && contextOptions && typeof contextOptions !== 'function' && contextOptions.error) { 2481 | exitCode = 1; 2482 | } 2483 | // message: do not have all displayed text available so only passing placeholder. 2484 | this._exit(exitCode, 'commander.help', '(outputHelp)'); 2485 | }; 2486 | 2487 | /** 2488 | * Add additional text to be displayed with the built-in help. 2489 | * 2490 | * Position is 'before' or 'after' to affect just this command, 2491 | * and 'beforeAll' or 'afterAll' to affect this command and all its subcommands. 2492 | * 2493 | * @param {string} position - before or after built-in help 2494 | * @param {string | Function} text - string to add, or a function returning a string 2495 | * @return {Command} `this` command for chaining 2496 | */ 2497 | addHelpText(position, text) { 2498 | const allowedValues = ['beforeAll', 'before', 'after', 'afterAll']; 2499 | if (!allowedValues.includes(position)) { 2500 | throw new Error(`Unexpected value for position to addHelpText. 2501 | Expecting one of '${allowedValues.join("', '")}'`); 2502 | } 2503 | const helpEvent = `${position}Help`; 2504 | this.on(helpEvent, (context) => { 2505 | let helpStr; 2506 | if (typeof text === 'function') { 2507 | helpStr = text({ error: context.error, command: context.command }); 2508 | } else { 2509 | helpStr = text; 2510 | } 2511 | // Ignore falsy value when nothing to output. 2512 | if (helpStr) { 2513 | context.write(`${helpStr}\n`); 2514 | } 2515 | }); 2516 | return this; 2517 | } 2518 | } 2519 | /** 2520 | * Expose the root command. 2521 | */ 2522 | 2523 | exports = module.exports = new Command(); 2524 | exports.program = exports; // More explicit access to global command. 2525 | 2526 | /** 2527 | * Expose classes 2528 | */ 2529 | 2530 | exports.Command = Command; 2531 | exports.Option = Option; 2532 | exports.CommanderError = CommanderError; 2533 | exports.InvalidOptionArgumentError = InvalidOptionArgumentError; 2534 | exports.Help = Help; 2535 | 2536 | /** 2537 | * Camel-case the given `flag` 2538 | * 2539 | * @param {string} flag 2540 | * @return {string} 2541 | * @api private 2542 | */ 2543 | 2544 | function camelcase(flag) { 2545 | return flag.split('-').reduce((str, word) => { 2546 | return str + word[0].toUpperCase() + word.slice(1); 2547 | }); 2548 | } 2549 | 2550 | /** 2551 | * Output help information if help flags specified 2552 | * 2553 | * @param {Command} cmd - command to output help for 2554 | * @param {Array} args - array of options to search for help flags 2555 | * @api private 2556 | */ 2557 | 2558 | function outputHelpIfRequested(cmd, args) { 2559 | const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag); 2560 | if (helpOption) { 2561 | cmd.outputHelp(); 2562 | // (Do not have all displayed text available so only passing placeholder.) 2563 | cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)'); 2564 | } 2565 | } 2566 | 2567 | /** 2568 | * Takes an argument and returns its human readable equivalent for help usage. 2569 | * 2570 | * @param {Object} arg 2571 | * @return {string} 2572 | * @api private 2573 | */ 2574 | 2575 | function humanReadableArgName(arg) { 2576 | const nameOutput = arg.name + (arg.variadic === true ? '...' : ''); 2577 | 2578 | return arg.required 2579 | ? '<' + nameOutput + '>' 2580 | : '[' + nameOutput + ']'; 2581 | } 2582 | 2583 | /** 2584 | * Parse the short and long flag out of something like '-m,--mixed ' 2585 | * 2586 | * @api private 2587 | */ 2588 | 2589 | function _parseOptionFlags(flags) { 2590 | let shortFlag; 2591 | let longFlag; 2592 | // Use original very loose parsing to maintain backwards compatibility for now, 2593 | // which allowed for example unintended `-sw, --short-word` [sic]. 2594 | const flagParts = flags.split(/[ |,]+/); 2595 | if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift(); 2596 | longFlag = flagParts.shift(); 2597 | // Add support for lone short flag without significantly changing parsing! 2598 | if (!shortFlag && /^-[^-]$/.test(longFlag)) { 2599 | shortFlag = longFlag; 2600 | longFlag = undefined; 2601 | } 2602 | return { shortFlag, longFlag }; 2603 | } 2604 | 2605 | /** 2606 | * Scan arguments and increment port number for inspect calls (to avoid conflicts when spawning new command). 2607 | * 2608 | * @param {string[]} args - array of arguments from node.execArgv 2609 | * @returns {string[]} 2610 | * @api private 2611 | */ 2612 | 2613 | function incrementNodeInspectorPort(args) { 2614 | // Testing for these options: 2615 | // --inspect[=[host:]port] 2616 | // --inspect-brk[=[host:]port] 2617 | // --inspect-port=[host:]port 2618 | return args.map((arg) => { 2619 | if (!arg.startsWith('--inspect')) { 2620 | return arg; 2621 | } 2622 | let debugOption; 2623 | let debugHost = '127.0.0.1'; 2624 | let debugPort = '9229'; 2625 | let match; 2626 | if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) { 2627 | // e.g. --inspect 2628 | debugOption = match[1]; 2629 | } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) { 2630 | debugOption = match[1]; 2631 | if (/^\d+$/.test(match[3])) { 2632 | // e.g. --inspect=1234 2633 | debugPort = match[3]; 2634 | } else { 2635 | // e.g. --inspect=localhost 2636 | debugHost = match[3]; 2637 | } 2638 | } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) { 2639 | // e.g. --inspect=localhost:1234 2640 | debugOption = match[1]; 2641 | debugHost = match[3]; 2642 | debugPort = match[4]; 2643 | } 2644 | 2645 | if (debugOption && debugPort !== '0') { 2646 | return `${debugOption}=${debugHost}:${parseInt(debugPort) + 1}`; 2647 | } 2648 | return arg; 2649 | }); 2650 | } 2651 | }); 2652 | commander.program; 2653 | commander.Command; 2654 | commander.Option; 2655 | commander.CommanderError; 2656 | commander.InvalidOptionArgumentError; 2657 | commander.Help; 2658 | 2659 | const program = new commander.Command() 2660 | .version('0.0.2') 2661 | .option('-c, --config ', 'config file path') // set config file path 2662 | .parse(process.argv); 2663 | program.parse(); 2664 | const options = program.opts(); 2665 | console.log(options); 2666 | new Entry({ command: program }).init().start(); 2667 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { minifierStyle } from './minify/minifyWxss'; 2 | import { minifyerWxml } from './minify/minifyWxml'; 3 | import commander from 'commander'; 4 | export declare class Entry { 5 | private DEFAULT_MINIPACK_CONFIG_PATH; 6 | private program; 7 | private config; 8 | constructor(data: { 9 | configPath?: string; 10 | command?: commander.Command; 11 | }); 12 | /** 13 | * init project 14 | */ 15 | init(): this; 16 | /** 17 | * setting bundler config 18 | */ 19 | setConfig(): void; 20 | /** 21 | * start build 22 | */ 23 | start(): Promise; 24 | /** 25 | * copy other asset files 26 | */ 27 | copyFile(): Promise; 28 | /** 29 | * watchFile 30 | */ 31 | watchFile(): void; 32 | } 33 | export declare const minifyStyle: typeof minifierStyle; 34 | export declare const minifyWxml: typeof minifyerWxml; 35 | export type { miniPackConfigOption } from './typings/config'; 36 | -------------------------------------------------------------------------------- /minipack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { minify } = require("html-minifier"); 3 | 4 | /** 5 | * 压缩HTML CSS文件 6 | */ 7 | function minifierStyle({ data,}) { 8 | return minify(data, { 9 | minifyCSS: true, 10 | removeComments: true, 11 | collapseWhitespace: true, 12 | keepClosingSlash: true, 13 | trimCustomFragments: true, 14 | caseSensitive: true, 15 | }) 16 | } 17 | 18 | function minifyWxml({ data, }) { 19 | return data.replace(/\n|\s{2,}/g, ' ').replace(/\/\/.*|/g, '') 20 | } 21 | 22 | /** 23 | * @type import('./dist/index').miniPackConfigOption 24 | */ 25 | module.exports = { 26 | watchEntry: path.resolve(__dirname, 'test','compileCode'), 27 | tsConfigPath: path.resolve(__dirname, 'tsconfig.json'), 28 | outDir: path.resolve(__dirname, 'build'), 29 | isWatch: true, 30 | typeRoots: [ 31 | path.resolve(__dirname, './typings'), 32 | path.resolve(__dirname, 'node_modules/@types/node') 33 | ], 34 | esBuildOptions: { 35 | sourcemap: true, 36 | }, 37 | plugins: [ 38 | { 39 | test: /.*\.(wxml)$/, 40 | action: minifyWxml, 41 | }, 42 | { 43 | test: /.*\.(wxss)$/, 44 | action: minifierStyle, 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weapp-minipack", 3 | "version": "0.1.20", 4 | "description": "a bundler for wechat miniprogram", 5 | "main": "./dist/bundle.js", 6 | "types": "./dist/index.d.ts", 7 | "bin": { 8 | "weapp-minipack": "./dist/bundle_command.js" 9 | }, 10 | "scripts": { 11 | "test:method": "node ./test/index.js", 12 | "test:command": "minipack -c ./minipack.config.js", 13 | "test:compile": "jest", 14 | "build": "yarn build:method & yarn build:commander & shx cp ./src/typings/config.d.ts ./dist/index.d.ts", 15 | "build:method": "cross-env NODE_ENV=production:method rollup --config ./rollup.config.js & ", 16 | "build:commander": "cross-env NODE_ENV=production:commander rollup --config ./rollup.config.js" 17 | }, 18 | "keywords": [ 19 | "wechat", 20 | "miniprogram", 21 | "typescript" 22 | ], 23 | "author": "Jerrmy Y", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/IchliebedichZhu/weapp-minipack.git" 28 | }, 29 | "dependencies": { 30 | "@types/html-minifier": "^4.0.0", 31 | "commander": "^7.2.0", 32 | "esbuild": "^0.12.7", 33 | "html-minifier": "^4.0.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/preset-env": "^7.14.4", 37 | "@babel/preset-typescript": "^7.13.0", 38 | "@rollup/plugin-typescript": "^8.2.1", 39 | "@types/commander": "^2.12.2", 40 | "@types/node": "^14.14.14", 41 | "cross-env": "^7.0.3", 42 | "jest": "^27.0.4", 43 | "rollup": "^2.50.4", 44 | "rollup-plugin-commonjs": "^10.1.0", 45 | "rollup-plugin-node-resolve": "^5.2.0", 46 | "rollup-plugin-replace": "^2.2.0", 47 | "rollup-plugin-typescript2": "^0.30.0", 48 | "shx": "^0.3.3", 49 | "ts-node": "^10.0.0", 50 | "tslib": "^2.0.3", 51 | "typescript": "^4.1.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const rollupTypescript = require('rollup-plugin-typescript2'); 3 | const resolve = require('rollup-plugin-node-resolve'); 4 | const commonjs = require('rollup-plugin-commonjs'); 5 | const env = process.env.NODE_ENV; 6 | 7 | const isCommander = /commander/g.test(env); 8 | 9 | /** 10 | * @type import('rollup').RollupOptions 11 | */ 12 | module.exports = { 13 | input: path.resolve(__dirname, isCommander ? 'src/command.ts' : 'src/index.ts'), 14 | output: { 15 | file: path.resolve(__dirname, 'dist', isCommander ? 'bundle_command.js' : 'bundle.js'), 16 | format: 'cjs', 17 | banner: isCommander ? '#!/usr/bin/env node' : '', 18 | }, 19 | external: [ 20 | 'fs', 'child_process', 'path', 'readline', 21 | 'events', 'esbuild', 'stream', 'html-minifier', 22 | 'crypto' 23 | ], 24 | plugins: [ 25 | resolve(), 26 | commonjs(), 27 | rollupTypescript({ 28 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 29 | useTsconfigDeclarationDir: true, 30 | }), 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/changeConfig.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync, statSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | const PROJECT_CONFIG_PATH = resolve(__dirname, '../project.config.json'); 4 | /** 5 | * 改变小程序配置 6 | */ 7 | export function changeMiniprogramConfig(config = {}, configPath = PROJECT_CONFIG_PATH) { 8 | if (existsSync(configPath)) { 9 | let data = readFileSync(configPath, { encoding: 'utf-8' }); 10 | try { 11 | data = JSON.parse(data); 12 | Object.assign(data, config); 13 | data = JSON.stringify(data).replace(/{/g, '{\r\n') 14 | .replace(/}/g, '}\r\n').replace(/,/g, ',\r\n') 15 | .replace(/\[/g, '[\r\n').replace(/\]/g, ']\r\n') 16 | writeFileSync(configPath, data); 17 | } catch(err) { 18 | console.error(err); 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * 注入环境变量 25 | * @param { String } path 26 | * @param { Array String } configFile 27 | * @param { String } env 28 | */ 29 | export function addEnv(rootPath: string, configFile: string[], env: string) { 30 | for (let x of configFile) { 31 | const file = resolve(rootPath, x); 32 | if (existsSync(file) && statSync(file).isFile()) { 33 | const data = readFileSync(file, { encoding: 'utf-8' }); 34 | console.log(env); 35 | 36 | writeFileSync(file, [env, '\r\n', data.replace(new RegExp(env, 'g'), '')].join('')) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from './index'; 2 | import commander from 'commander'; 3 | 4 | const program = new commander.Command() 5 | .version('0.0.2') 6 | .option('-c, --config ', 'config file path',) // set config file path 7 | .parse(process.argv); 8 | 9 | program.parse(); 10 | 11 | const options = program.opts(); 12 | 13 | console.log(options); 14 | 15 | new Entry({ command: program }).init().start(); 16 | -------------------------------------------------------------------------------- /src/compile/compile.ts: -------------------------------------------------------------------------------- 1 | import esbuild, { build } from "esbuild"; 2 | import { resolve } from "path"; 3 | import { addEnv, changeMiniprogramConfig } from "../changeConfig"; 4 | import { handleAssetsFile } from "../controlFile/handleAssetsFile"; 5 | import { readTsFile, startCompile } from "../controlFile/readFile"; 6 | import { InpouringEnvOtion } from "../typings/config"; 7 | import { filterObject } from "../utils/utils"; 8 | 9 | /** 10 | * 压缩代码主方法 11 | * @param options esbuild options 12 | * @returns 13 | */ 14 | export async function translateCode(options: esbuild.BuildOptions) { 15 | try { 16 | const result = await build(options); 17 | return result; 18 | } catch (err) { 19 | console.log("build err", err); 20 | return false; 21 | } 22 | } 23 | 24 | /** 25 | * 编译TS文件 26 | */ 27 | export async function actionCompileTsFile( 28 | tsFile: miniPack.ITsFileData[], 29 | rootPath: string, 30 | copyPath: string, 31 | inpourEnv: InpouringEnvOtion, 32 | esBuildOptions: esbuild.BuildOptions 33 | ) { 34 | console.log("正在编译指定文件"); 35 | console.time("compile"); 36 | for (let compileFile of tsFile) { 37 | const sourchFile = resolve(rootPath, compileFile.filename); 38 | let compilePath: string | string[] = resolve(copyPath, compileFile.filename) 39 | .replace(/\\/g, "/") 40 | .split("/"); 41 | compilePath.splice(compilePath.length - 1, 1); 42 | compilePath = compilePath.join("/"); 43 | 44 | const result = await translateCode({ 45 | format: "cjs", 46 | entryPoints: [sourchFile], 47 | minify: true, 48 | outdir: compilePath, 49 | ...esBuildOptions, 50 | }); 51 | console.log(result); 52 | if (inpourEnv.isInpour) { 53 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 54 | } 55 | } 56 | console.log("编译完成"); 57 | console.timeEnd("compile"); 58 | } 59 | 60 | /** 61 | * 监听文件开始编译 62 | */ 63 | export async function actionCompile( 64 | fileArr: { type: string; event: string; filename: string }[], 65 | option: miniPack.IWatchFileOption 66 | ) { 67 | const { 68 | rootPath, 69 | inpourEnv, 70 | miniprogramProjectConfig, 71 | miniprogramProjectPath, 72 | plugins = [], 73 | esBuildOptions, 74 | } = option; 75 | let { copyPath } = option; 76 | 77 | // 对象去重 78 | fileArr = filterObject(fileArr); 79 | // 判断是否有文件新增或删除 80 | const isReadName = fileArr.filter((val) => val.event === "rename").length > 0; 81 | // ts之外的文件 82 | const assetsFile = fileArr.filter((val) => val.type === "asset"); 83 | // ts文件 84 | const tsFile = fileArr.filter((val) => val.type === "ts"); 85 | 86 | console.log("assetsFile", assetsFile); 87 | 88 | // 有文件新增或删除为重新编译 89 | if (isReadName) { 90 | const fileList = readTsFile(rootPath); 91 | const compileResult = await translateCode({ 92 | format: "cjs", 93 | entryPoints: fileList, 94 | minify: true, 95 | outdir: copyPath, 96 | ...esBuildOptions, 97 | }); 98 | if (compileResult) { 99 | if (inpourEnv.isInpour) { 100 | addEnv(copyPath, inpourEnv.files, inpourEnv.data); 101 | } 102 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 103 | } 104 | // 重新写入文件 105 | startCompile(rootPath, copyPath); 106 | } else { 107 | // 写入ts文件 108 | if (tsFile.length) { 109 | await actionCompileTsFile( 110 | tsFile, 111 | rootPath, 112 | copyPath, 113 | inpourEnv, 114 | esBuildOptions 115 | ); 116 | } 117 | 118 | // 写入修改的文件 119 | for (let assetFile of assetsFile) { 120 | if (assetFile && assetFile.filename) { 121 | handleAssetsFile( 122 | resolve(rootPath, assetFile.filename), 123 | resolve(copyPath, assetFile.filename), 124 | plugins 125 | ); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * default config 3 | */ 4 | import { resolve } from 'path'; 5 | import { miniPackConfigOption } from './typings/config'; 6 | 7 | const config: miniPackConfigOption = { 8 | env: process.env.NODE_ENV || 'none', 9 | watchEntry: '', 10 | outDir: resolve(process.cwd(), 'dist'), 11 | isTs: true, 12 | tsConfigPath: '', 13 | miniprogramProjectPath: resolve(process.cwd(), '../project.config.json'), 14 | miniprogramProjectConfig: {}, 15 | isWatch: false, 16 | inpouringEnv: { 17 | isInpour: false, 18 | files: [], 19 | data: '', 20 | }, 21 | typeRoots: [], 22 | plugins: [], 23 | } 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/controlFile/checkFile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createReadStream, 3 | readFileSync, 4 | statSync, 5 | } from 'fs'; 6 | import readLine from 'readline'; 7 | import crypto from 'crypto'; 8 | const IMPORT_REG = /import.*from.*/; 9 | 10 | /** 11 | * 判断是否是文件夹 12 | */ 13 | export function checkIsDir(filePath: string) { 14 | return statSync(filePath).isDirectory(); 15 | } 16 | 17 | /** 18 | * 判断两个文件大小是否相等 19 | */ 20 | export function checkFileIsSame(pathA: string, pathB: string) { 21 | ; 22 | const fileA_MD5 = crypto.createHash('md5').update(readFileSync(pathA)).digest('hex'); 23 | const fileB_MD5 = crypto.createHash('md5').update(readFileSync(pathB)).digest('hex'); 24 | return fileA_MD5 === fileB_MD5; 25 | } 26 | 27 | /** 28 | * 查看文件是否有import 29 | * @param filePath 文件路径 30 | */ 31 | export function checkIsImport(filePath: string): Promise{ 32 | return new Promise(finished => { 33 | if (checkIsDir(filePath)) finished(false); 34 | const readStream = createReadStream(filePath); 35 | const rl = readLine.createInterface(readStream); 36 | let isImport = false; 37 | rl.on('line', (lineData) => { 38 | if (IMPORT_REG.test(lineData)) { 39 | isImport = true; 40 | rl.close(); 41 | } 42 | }) 43 | rl.on('close', () => { 44 | finished(isImport); 45 | }) 46 | }) 47 | 48 | } -------------------------------------------------------------------------------- /src/controlFile/handleAssetsFile.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync, statSync } from "fs"; 2 | import { copyFile } from "./readFile"; 3 | 4 | export function handleAssetsFile(tmpPath: string, endPath: string, plugins: PluginFunction[]) 5 | : boolean 6 | { 7 | let formatData = ''; 8 | for(let x of plugins) { 9 | if( 10 | x.test.test(tmpPath) && 11 | existsSync(tmpPath) && 12 | statSync(tmpPath).isFile() 13 | ) { 14 | const data = readFileSync(tmpPath, { encoding: 'utf-8' }); 15 | const actionData: miniPack.IPluginOption = { 16 | copyDir: endPath, 17 | filePath: tmpPath, 18 | data, 19 | dataBuf: Buffer.alloc(data.length, data), 20 | } 21 | formatData = x.action(actionData) 22 | break; 23 | } 24 | } 25 | 26 | if (formatData) { 27 | writeFileSync(endPath, formatData); 28 | } else { 29 | copyFile(tmpPath, endPath); 30 | } 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /src/controlFile/readFile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | readdirSync, existsSync, createReadStream, createWriteStream, 3 | mkdirSync, 4 | } from 'fs'; 5 | import { resolve } from 'path'; 6 | import { checkFileIsSame, checkIsDir } from './checkFile'; 7 | import { EXPLORE_REG, TS_REG } from '../globalConfig'; 8 | import { handleAssetsFile } from './handleAssetsFile'; 9 | import type { PluginFunction } from '../typings/config'; 10 | 11 | /** 12 | * 读取文件夹 13 | */ 14 | function readDir(filePath: string) { 15 | if (checkIsDir(filePath)) { 16 | return readdirSync(filePath); 17 | } else { 18 | return []; 19 | } 20 | } 21 | 22 | /** 23 | * 获取文件夹内所有文件 24 | */ 25 | export function getDirAllFile(filePath: string) { 26 | let fileArr: string[] = []; 27 | 28 | if (checkIsDir(filePath)) { 29 | const fileList = readdirSync(filePath); 30 | 31 | fileList.forEach(val => { 32 | const tmpPath = resolve(filePath,val) 33 | if (checkIsDir(tmpPath)) { 34 | fileArr = fileArr.concat(getDirAllFile(tmpPath)); 35 | } else { 36 | fileArr.push(tmpPath); 37 | } 38 | }) 39 | return fileArr 40 | } 41 | return []; 42 | } 43 | 44 | /** 45 | * 用流的方式复制文件 46 | */ 47 | export function copyFile(beginPath: string, endPath: string) { 48 | if(existsSync(beginPath) && !checkIsDir(beginPath)) { 49 | const readStream = createReadStream(beginPath); 50 | const writeStream = createWriteStream(endPath); 51 | readStream.pipe(writeStream); 52 | } 53 | } 54 | 55 | /** 56 | * 创建文件夹 57 | */ 58 | function createDir(filePath: string) { 59 | if (!existsSync(filePath)) { 60 | mkdirSync(filePath); 61 | } 62 | } 63 | 64 | /** 65 | * 开始编译 66 | */ 67 | export async function startCompile( 68 | filePath: string, copyPath: string, plugins: PluginFunction[] = [] 69 | ) { 70 | // 读取所有文件 71 | const fileArr = readDir(filePath); 72 | for (let x of fileArr) { 73 | const tmpPath = resolve(resolve(filePath, x)); 74 | let endPath = resolve(copyPath, x); 75 | if (checkIsDir(tmpPath)) { 76 | createDir(endPath); 77 | startCompile(tmpPath, endPath, plugins); 78 | } else if (!EXPLORE_REG.test(endPath) || /\/lib\/.*|\lib\.*/g.test(endPath)) { 79 | if (existsSync(endPath) && checkFileIsSame(tmpPath, endPath)) { 80 | continue; 81 | } else { 82 | handleAssetsFile(tmpPath, endPath, plugins); 83 | } 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * 读取所有ts文件 90 | */ 91 | export function readTsFile(filePath: string, currentPath = '') { 92 | const fileArr = readDir(filePath); 93 | let resultArr: string[] = []; 94 | for (let x of fileArr) { 95 | const tmpPath = resolve(filePath, x); 96 | const keyPath = `${ currentPath }/${ x }`; 97 | if (checkIsDir(tmpPath)) { 98 | resultArr = resultArr.concat(readTsFile(tmpPath, keyPath)); 99 | } else { 100 | if (TS_REG.test(keyPath)) { 101 | resultArr.push(tmpPath); 102 | } 103 | } 104 | } 105 | return resultArr; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/controlFile/watchFile.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "fs"; 2 | import { actionCompile } from "../compile/compile"; 3 | import { EXPLORE_REG } from "../globalConfig"; 4 | 5 | /** 6 | * 监听文件改动 7 | */ 8 | let changeFileArr: { type: string, event: string, filename: string }[] = []; 9 | let watchFileTimer: any = null; 10 | export function watchFile(option: miniPack.IWatchFileOption, during: number = 500) { 11 | console.log('开始监听文件'); 12 | const { rootPath, } = option; 13 | watch(rootPath, { recursive: true, }, (event, filename) => { 14 | clearTimeout(watchFileTimer); 15 | changeFileArr.push({ 16 | type: EXPLORE_REG.test(filename) ? 'ts' : 'asset', 17 | event, 18 | filename, 19 | }) 20 | 21 | watchFileTimer = setTimeout(() => { 22 | actionCompile(changeFileArr, option); 23 | changeFileArr = []; 24 | }, during); 25 | }) 26 | } -------------------------------------------------------------------------------- /src/globalConfig.ts: -------------------------------------------------------------------------------- 1 | export const EXPLORE_REG = new RegExp(".*.(js|ts)$|.DS_Store"); 2 | export const TS_REG = /.*\.ts$/; 3 | export const HTML_CSS_REG = /.*\.(wxss)$/; 4 | 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { statSync, existsSync, } from 'fs'; 3 | import DEFAULT_CONFIG from './config'; 4 | 5 | import { readTsFile, startCompile } from './controlFile/readFile'; 6 | import { addEnv, changeMiniprogramConfig, } from './changeConfig'; 7 | import { translateCode, } from './compile/compile'; 8 | import { watchFile, } from './controlFile/watchFile'; 9 | import { minifierStyle, } from './minify/minifyWxss'; 10 | import { minifyerWxml, } from './minify/minifyWxml'; 11 | import { miniPackConfigOption } from './typings/config'; 12 | import commander from 'commander'; 13 | 14 | export class Entry { 15 | private DEFAULT_MINIPACK_CONFIG_PATH: string; 16 | private program: null | commander.Command; 17 | private config: miniPackConfigOption 18 | constructor(data: {configPath?: string, command?: commander.Command}) { 19 | const { configPath, command } = data; 20 | this.program = command || null; 21 | this.DEFAULT_MINIPACK_CONFIG_PATH = configPath || resolve(__dirname, '../minipack.config.js') 22 | this.config = DEFAULT_CONFIG; 23 | } 24 | 25 | /** 26 | * init project 27 | */ 28 | init() { 29 | this.setConfig(); 30 | return this; 31 | } 32 | 33 | /** 34 | * setting bundler config 35 | */ 36 | setConfig() { 37 | let file = this.DEFAULT_MINIPACK_CONFIG_PATH; 38 | if (this.program) { 39 | // get config file 40 | const options = this.program.opts(); 41 | if (!options.config) { 42 | options.config = this.DEFAULT_MINIPACK_CONFIG_PATH; 43 | } else { 44 | const isFullPath = /^\/.*/.test(options.config); 45 | file = isFullPath ? options.config : resolve(process.cwd(), options.config); 46 | } 47 | } 48 | 49 | if (existsSync(file) && statSync(file).isFile()) { 50 | try { 51 | let data = require(file); 52 | Object.assign(this.config, data); 53 | if (!this.config.tsConfigPath) throw new Error('tsConfigPath must defined'); 54 | if (!existsSync(file) || !statSync(file).isFile()) throw new Error('tsConfigPath path is not found'); 55 | 56 | } catch(err) { 57 | throw new Error(err.toString()) 58 | } 59 | } else { 60 | throw new Error(`config file ${ file } is not defined`) 61 | } 62 | } 63 | 64 | /** 65 | * start build 66 | */ 67 | async start() { 68 | const { 69 | watchEntry, outDir, inpouringEnv, esBuildOptions, 70 | } = this.config; 71 | 72 | console.log('compile start'); 73 | const fileList = readTsFile(watchEntry) 74 | const compileResult = await translateCode({ 75 | format: 'cjs', 76 | entryPoints: fileList, 77 | minify: true, 78 | outdir: outDir, 79 | ...esBuildOptions, 80 | }) 81 | // const result = childProcess.spawnSync(`tsc`,[`--project`, tsConfigPath, '--outDir', outDir,], { shell: true, }); 82 | if (compileResult) { 83 | console.log('compile finished'); 84 | if (inpouringEnv.isInpour) { 85 | console.log('start inpour data'); 86 | addEnv(outDir, inpouringEnv.files, inpouringEnv.data); 87 | console.log('inpour finished'); 88 | } 89 | 90 | await this.copyFile(); 91 | this.watchFile(); 92 | } else { 93 | return; 94 | } 95 | } 96 | 97 | /** 98 | * copy other asset files 99 | */ 100 | copyFile() { 101 | return new Promise(async truly => { 102 | const { 103 | watchEntry, outDir, miniprogramProjectConfig, 104 | miniprogramProjectPath, plugins, 105 | } = this.config; 106 | console.log('start copy asset files'); 107 | await startCompile(watchEntry, outDir, plugins); 108 | changeMiniprogramConfig(miniprogramProjectConfig, miniprogramProjectPath); 109 | console.log('copy assets success'); 110 | truly(true); 111 | }) 112 | 113 | } 114 | 115 | /** 116 | * watchFile 117 | */ 118 | watchFile() { 119 | const { 120 | isWatch, watchEntry, outDir, tsConfigPath, 121 | miniprogramProjectConfig, miniprogramProjectPath, 122 | inpouringEnv, typeRoots, plugins, esBuildOptions = {}, 123 | } = this.config; 124 | 125 | if (isWatch) { 126 | const watchOption: miniPack.IWatchFileOption = { 127 | rootPath: watchEntry, 128 | copyPath: outDir, 129 | tsconfigPath: tsConfigPath, 130 | inpourEnv: inpouringEnv, 131 | miniprogramProjectPath, 132 | miniprogramProjectConfig, 133 | typingDirPath: typeRoots, 134 | plugins, 135 | esBuildOptions, 136 | } 137 | watchFile(watchOption); 138 | } 139 | } 140 | } 141 | 142 | export const minifyStyle = minifierStyle; 143 | export const minifyWxml = minifyerWxml; 144 | -------------------------------------------------------------------------------- /src/minify/minifyWxml.ts: -------------------------------------------------------------------------------- 1 | 2 | export function minifyerWxml({ data, }: miniPack.IPluginOption): string { 3 | return data.replace(/\n|\s{2,}/g, ' ').replace(/\/\/.*|/g, '') 4 | } 5 | -------------------------------------------------------------------------------- /src/minify/minifyWxss.ts: -------------------------------------------------------------------------------- 1 | import { minify } from "html-minifier"; 2 | 3 | /** 4 | * 压缩HTML CSS文件 5 | * @param filePath 6 | * @param endPath 7 | * @returns 8 | */ 9 | export function minifierStyle({ data, }: miniPack.IPluginOption): string { 10 | return minify(data, { 11 | minifyCSS: true, 12 | removeComments: true, 13 | collapseWhitespace: true, 14 | keepClosingSlash: true, 15 | trimCustomFragments: true, 16 | caseSensitive: true, 17 | }) 18 | } -------------------------------------------------------------------------------- /src/typings/config.d.ts: -------------------------------------------------------------------------------- 1 | import type esbuild from 'esbuild'; 2 | 3 | interface IPluginOption { 4 | copyDir: string 5 | filePath: string 6 | data: string 7 | dataBuf: Buffer 8 | } 9 | 10 | type PluginFunction = { 11 | test: RegExp, 12 | action: (data: IPluginOption) => string 13 | } 14 | 15 | export interface InpouringEnvOtion { 16 | /** 17 | * control inpour env data or not 18 | */ 19 | isInpour: boolean 20 | /** 21 | * inpour files 22 | */ 23 | files: string[] 24 | 25 | /** 26 | * inpour data 27 | */ 28 | data: string 29 | } 30 | 31 | export interface miniPackConfigOption { 32 | /** 33 | * current bundler env 34 | */ 35 | env: string 36 | 37 | /** 38 | * your program's watch file entry path 39 | */ 40 | watchEntry: string 41 | 42 | /** 43 | * output file root path 44 | */ 45 | outDir: string 46 | 47 | /** 48 | * typescript's config file path 49 | * if param "isTs" is false, you can ignore it 50 | */ 51 | tsConfigPath: string 52 | 53 | /** 54 | * set use program language 55 | */ 56 | isTs: boolean 57 | 58 | /** 59 | * wechat miniprogram project.config.json path 60 | */ 61 | miniprogramProjectPath: string 62 | 63 | /** 64 | * wechat miniProgram project.config.json param 65 | * you can change project config after the first bundle finished 66 | */ 67 | miniprogramProjectConfig: Record 68 | 69 | /** 70 | * inpouring env data option 71 | */ 72 | inpouringEnv: InpouringEnvOtion 73 | 74 | /** 75 | * whether watch file change to compile or not 76 | */ 77 | isWatch: boolean 78 | 79 | /** 80 | * tsconfig typeRoots 81 | */ 82 | typeRoots: string[] 83 | 84 | /** 85 | * plugins 86 | */ 87 | plugins?: PluginFunction[] 88 | 89 | /** 90 | * compile options 91 | */ 92 | esBuildOptions?: esbuild.BuildOptions 93 | } 94 | 95 | import commander from 'commander'; 96 | export declare class Entry { 97 | private DEFAULT_MINIPACK_CONFIG_PATH; 98 | private program; 99 | private config; 100 | constructor(data: { 101 | configPath?: string; 102 | command?: commander.Command; 103 | }); 104 | /** 105 | * init project 106 | */ 107 | init(): this; 108 | /** 109 | * setting bundler config 110 | */ 111 | setConfig(): void; 112 | /** 113 | * start build 114 | */ 115 | start(): Promise; 116 | /** 117 | * copy other asset files 118 | */ 119 | copyFile(): Promise; 120 | /** 121 | * watchFile 122 | */ 123 | watchFile(): void; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | type PluginFunction = { 2 | test: RegExp, 3 | action: (data: IPluginOption) => string 4 | } 5 | 6 | declare module miniPack { 7 | 8 | interface IPluginOption { 9 | copyDir: string 10 | filePath: string 11 | data: string 12 | dataBuf: buffer 13 | } 14 | 15 | interface IWatchFileOption { 16 | /** 17 | * entry path 18 | */ 19 | rootPath: string 20 | /** 21 | * outputDir 22 | */ 23 | copyPath: string 24 | /** 25 | * ts config file 26 | */ 27 | tsconfigPath: string 28 | 29 | /** 30 | * typingDirPath 31 | */ 32 | typingDirPath: string[] 33 | 34 | /** 35 | * inpour environment data 36 | */ 37 | inpourEnv: InpouringEnvOtion 38 | 39 | /** 40 | * project.config.js json 41 | */ 42 | miniprogramProjectConfig: Record 43 | 44 | /** 45 | * miniprogram project config path 46 | */ 47 | miniprogramProjectPath: string 48 | /** 49 | * assets file handle plugins 50 | */ 51 | plugins?: PluginFunction[] 52 | /** 53 | * compile options 54 | */ 55 | esBuildOptions: esbuild.BuildOptions 56 | } 57 | 58 | interface ITsFileData { 59 | /** 60 | * 文件类型 61 | */ 62 | type: string; 63 | /** 64 | * 执行的文件操作 65 | */ 66 | event: string; 67 | /** 68 | * 文件名 69 | */ 70 | filename: string; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // 对象数组去重 2 | export function filterObject(arr: miniPack.ITsFileData[]): miniPack.ITsFileData[] { 3 | const obj: Record = {}; 4 | const result: miniPack.ITsFileData[] = []; 5 | arr.forEach(val => { 6 | const key = `${ val.type }_${ val.event }_${ val.filename }` 7 | if (!obj[ key ]) { 8 | obj[ key ] = 1; 9 | result.push(val); 10 | } 11 | }) 12 | return result; 13 | } -------------------------------------------------------------------------------- /test/compile.test.js: -------------------------------------------------------------------------------- 1 | import { translateCode } from '../src/compile/compile'; 2 | // import readFile from '../src/readFile'; 3 | import path from 'path'; 4 | 5 | const entry = [ 6 | path.resolve(__dirname, 'compileCode', 'index.ts'), 7 | path.resolve(__dirname, 'compileCode', 'a.ts'), 8 | path.resolve(__dirname, 'compileCode', 'child', 'child.ts'), 9 | path.resolve(__dirname, 'compileCode', 'child', 'child2', 'child2.ts'), 10 | ] 11 | 12 | /** 13 | * 测试esbuild编译代码 14 | */ 15 | test('translate code', async () => { 16 | const result = await translateCode({ 17 | entryPoints: entry, 18 | minify: true, 19 | outdir: path.resolve(__dirname, 'build'), 20 | }) 21 | expect(result).toBeTruthy(); 22 | }); 23 | 24 | // /** 25 | // * 测试读取所有文件 26 | // */ 27 | // test('get file path', async () => { 28 | // const fileDir = path.resolve(__dirname, 'compileCode'); 29 | // const arr = readFile.getDirAllFile(fileDir); 30 | // expect(true).toBeTruthy(); 31 | // }) 32 | 33 | // /** 34 | // * 测试读取所有文件 35 | // */ 36 | // test('get file path', async () => { 37 | // const fileDir = path.resolve(__dirname, 'compileCode'); 38 | // const arr = readFile.getDirAllFile(fileDir); 39 | // expect(true).toBeTruthy(); 40 | // }) 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/compileCode/a.ts: -------------------------------------------------------------------------------- 1 | const best = 22222; 2 | const best2 = 33333; 3 | console.log(best); 4 | console.log(best2); 5 | -------------------------------------------------------------------------------- /test/compileCode/child/child.ts: -------------------------------------------------------------------------------- 1 | export const child = 11111; 2 | 3 | -------------------------------------------------------------------------------- /test/compileCode/child/child2/child2.ts: -------------------------------------------------------------------------------- 1 | import { child } from '../child'; 2 | const child2 = 11111; 3 | const child3 = 11111; 4 | console.log(child); 5 | 6 | console.log(child2); 7 | console.log(child3); 8 | -------------------------------------------------------------------------------- /test/compileCode/index.ts: -------------------------------------------------------------------------------- 1 | const data = 111; 2 | 3 | console.log(data); 4 | -------------------------------------------------------------------------------- /test/compileCode/test.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | {{ totalNum }} 18 | {{ totalLabel }} 19 | 20 | 21 | 26 | {{ startTime.time + ' ' + startTime.week }} 至 {{ endTime.time + ' ' + endTime.week }} 27 | 28 | 29 | 30 | 31 | 32 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 57 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /test/compileCode/test.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .main { 3 | background: #f9f9f9; 4 | font-family: 'font-light'; 5 | } 6 | .notify { 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | z-index: 100; 11 | } 12 | 13 | .close_icon { 14 | position: absolute; 15 | bottom: -80rpx; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { Entry } = require('../dist/bundle'); 2 | const path = require('path'); 3 | 4 | const pack = new Entry({ configPath: path.join(__dirname, '..' ,'minipack.config.js') }); 5 | 6 | pack.init().start(); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "declaration": false, 12 | "typeRoots": [ 13 | "./src/typings" 14 | ] 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------