├── .commitlintrc.js ├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── README.md ├── lerna.json ├── nx.json ├── package-lock.json ├── package.json ├── packages └── tiny │ ├── .babelrc.js │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .npmignore │ ├── README.md │ ├── __test__ │ └── index.spec.ts │ ├── bin │ └── index.js │ ├── index.js │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── features │ │ ├── index.ts │ │ ├── process.ts │ │ └── proto.ts │ ├── index.ts │ └── interface.ts │ └── tsconfig.json └── scripts ├── build ├── config.ts └── index.ts ├── create ├── config.ts └── index.ts ├── link ├── config.ts ├── link.ts └── unlink.ts ├── temp ├── .babelrc.js ├── .eslintignore ├── .eslintrc.js ├── .npmignore ├── README.md ├── __test__ │ └── index.spec.ts ├── jest.config.js ├── package.json ├── rollup.config.js ├── src │ ├── features │ │ └── index.ts │ └── index.ts └── tsconfig.json ├── test ├── config.ts └── index.ts └── utils.ts /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * feat:新增功能 3 | * fix:修复bug 4 | * docs:修改文档 5 | * refactor:代码重构,未新增任何功能和修复任何bug 6 | * build:改变构建流程,新增依赖库、工具等(例如webpack修改) 7 | * style:仅仅修改了空格、缩进等,不改变代码逻辑 8 | * perf:改善性能和体现的修改 9 | * ci:自动化流程配置修改 10 | * revert:回滚到上一个版本 11 | **/ 12 | module.exports = { 13 | extends: ['@commitlint/config-conventional'], 14 | rules: { 15 | 'type-enum': [ 16 | 2, 17 | 'always', 18 | [ 19 | 'feat', 20 | 'fix', 21 | 'docs', 22 | 'refactor', 23 | 'build', 24 | 'style', 25 | 'pref', 26 | 'ci', 27 | 'revert' 28 | ] 29 | ], 30 | 'type-case': [0], 31 | 'type-empty': [0], 32 | 'scope-empty': [0], 33 | 'scope-case': [0], 34 | 'subject-full-stop': [0, 'never'], 35 | 'subject-case': [0, 'never'], 36 | 'header-max-length': [0, 'always', 72] 37 | }, 38 | ignores: [ 39 | commit => { 40 | // 忽略掉lerna本身的commit 41 | if (new RegExp(/^(Publish)/).test(commit)) return true 42 | return false 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/dist 4 | **/coverage 5 | .DS_Store 6 | lerna-debug.log -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 文章地址:[这样用 lerna 也太爽了吧!](https://juejin.cn/post/7134646424083365924) 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "useNx": true, 4 | "version": "independent", 5 | "useWorkspaces": true, 6 | "command": { 7 | "bootstrap": { 8 | "hoist": [ 9 | "@babel/plugin-transform-runtime", 10 | "@babel/preset-env", 11 | "@babel/preset-typescript", 12 | "@rollup/plugin-babel", 13 | "@rollup/plugin-eslint", 14 | "@rollup/plugin-node-resolve", 15 | "@types/jest", 16 | "@typescript-eslint/eslint-plugin", 17 | "@typescript-eslint/parser", 18 | "eslint", 19 | "jest", 20 | "jest-environment-jsdom", 21 | "lint-staged", 22 | "prettier", 23 | "rollup", 24 | "rollup-plugin-terser", 25 | "rollup-plugin-typescript2", 26 | "ts-jest", 27 | "tslib", 28 | "typescript" 29 | ] 30 | }, 31 | "version": { 32 | "message": "feat: publish" 33 | } 34 | }, 35 | "ignoreChanges": [ 36 | "**/coverage/**", 37 | "**/__tests__/**", 38 | "**/*.md", 39 | "**/dist", 40 | "**/node_module" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": ["build", "test"] 7 | } 8 | } 9 | }, 10 | "targetDefault": { 11 | "build": { 12 | "dependsOn": ["^build"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lerna-npm", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "create": "ts-node scripts/create/index.ts", 9 | "test": "ts-node scripts/test/index.ts", 10 | "build": "ts-node scripts/build/index.ts", 11 | "link": "ts-node scripts/link/link.ts", 12 | "unlink": "ts-node scripts/link/unlink.ts", 13 | "release": "lerna publish from-package", 14 | "version": "lerna version" 15 | }, 16 | "devDependencies": { 17 | "@commitlint/cli": "^17.0.3", 18 | "@commitlint/config-conventional": "^17.0.3", 19 | "@types/inquirer": "^9.0.1", 20 | "@types/metalsmith": "^2.3.1", 21 | "@types/node": "^18.7.6", 22 | "@types/rimraf": "^3.0.2", 23 | "husky": "^4.2.3", 24 | "inquirer": "^8.0.0", 25 | "lerna": "^5.1.8", 26 | "metalsmith": "^2.5.0", 27 | "nx": "^14.4.3", 28 | "ts-node": "^10.9.1" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/tiny/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/typescript', 4 | // '@babel/preset-typescript', 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: { 9 | browsers: ['ie >= 11'] 10 | }, 11 | // exclude: ['transform-async-to-generator', 'transform-regenerator'], 12 | modules: false // 不用转化esm 13 | // loose: true // 不启用松散模式 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/tiny/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /packages/tiny/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | 4 | plugins: ['@typescript-eslint'], 5 | 6 | rules: { 7 | 'no-unused-vars': 'off', 8 | '@typescript-eslint/no-unused-expressions': 'off', 9 | '@typescript-eslint/no-unused-vars': [ 10 | 'off', 11 | { 12 | vars: 'all', 13 | args: 'after-used', 14 | ignoreRestSiblings: true, 15 | argsIgnorePattern: '^_', 16 | varsIgnorePattern: '^_' 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/tiny/.npmignore: -------------------------------------------------------------------------------- 1 | __test__ 2 | node_mudules 3 | src 4 | test 5 | .babelrc.js 6 | .commitlintrc.js 7 | .editorconfig 8 | .eslintrc.js 9 | .eslintignore 10 | .gitignore 11 | .npmrc 12 | .prettierrc.json 13 | jest.config.js 14 | package-lock.json 15 | tsconfig.json 16 | rollup.config.js -------------------------------------------------------------------------------- /packages/tiny/README.md: -------------------------------------------------------------------------------- 1 | # yx-tiny 2 | 3 | 图片压缩工具 4 | 5 | 掘金文章:[图片不压缩,前端要背锅 🍳](https://juejin.cn/post/7153086294409609229) 6 | 7 | ## 用途 8 | 9 | `yx-tiny`是一个**基于tiny**、**自动化**的图片压缩工具(支持svga压缩以及识别已压缩的文件)。 10 | 11 | ## 如何使用 12 | 1. 安装 13 | 14 | ``` 15 | $ npm i -D yx-tiny 16 | ``` 17 | 18 | 2. 运行 19 | 20 | 21 | ```js 22 | $ npx tiny 23 | ``` 24 | or 25 | ```js 26 | scripts: { 27 | 'tiny': 'npx tiny' 28 | } 29 | ``` 30 | ``` 31 | $ npm run tiny 32 | ``` 33 | 34 | 3. 输入目标文件夹 35 | 4. 选择命中的文件夹 36 | 5. 选择压缩模式 37 | 6. 完成压缩 -------------------------------------------------------------------------------- /packages/tiny/__test__/index.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaneasy/lerna-npm/2caa6b9ae6684d759d8ebb301947ad78e243a5d5/packages/tiny/__test__/index.spec.ts -------------------------------------------------------------------------------- /packages/tiny/bin/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const init = require('../index') 4 | init() 5 | -------------------------------------------------------------------------------- /packages/tiny/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./dist/tiny.cjs.prod') 4 | -------------------------------------------------------------------------------- /packages/tiny/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | coverageProvider: 'v8', 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: './tsconfig.json' 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/tiny/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yx-tiny", 3 | "version": "0.0.10", 4 | "description": "图片压缩工具", 5 | "keywords": [ 6 | "npm", 7 | "yx", 8 | "tiny" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/yxichan/lerna-npm.git" 13 | }, 14 | "license": "ISC", 15 | "author": "chenyuxi", 16 | "main": "dist/tiny.cjs.prod.js", 17 | "module": "dist/tiny.esm.prod.js", 18 | "types": "dist/types/index.d.ts", 19 | "bin": { 20 | "tiny": "./bin/index.js" 21 | }, 22 | "scripts": { 23 | "clean": "rimraf dist", 24 | "lint": "eslint src/* --ext .js,.ts", 25 | "test": "jest", 26 | "prettier": "prettier --write src", 27 | "build": "npm run prettier && npm run lint && npm run clean && rollup -c" 28 | }, 29 | "devDependencies": { 30 | "@babel/plugin-transform-runtime": "^7.18.9", 31 | "@babel/preset-env": "^7.18.9", 32 | "@babel/preset-typescript": "^7.18.6", 33 | "@rollup/plugin-babel": "^5.3.1", 34 | "@rollup/plugin-commonjs": "^22.0.1", 35 | "@rollup/plugin-eslint": "^8.0.2", 36 | "@rollup/plugin-node-resolve": "^13.3.0", 37 | "@types/chalk": "2.2.0", 38 | "@types/inquirer": "8.2.1", 39 | "@types/jest": "^28.1.6", 40 | "@typescript-eslint/eslint-plugin": "^5.30.7", 41 | "@typescript-eslint/parser": "^5.30.7", 42 | "eslint": "^8.20.0", 43 | "jest": "^28.1.3", 44 | "jest-environment-jsdom": "^28.1.3", 45 | "prettier": "^2.7.1", 46 | "rollup": "^2.64.0", 47 | "rollup-plugin-terser": "^7.0.2", 48 | "rollup-plugin-typescript2": "^0.32.1", 49 | "ts-jest": "^28.0.7", 50 | "tslib": "^2.4.0", 51 | "typescript": "^4.7.4" 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 56 | } 57 | }, 58 | "dependencies": { 59 | "@babel/runtime": "^7.18.9", 60 | "chalk": "4.1.0", 61 | "console-grid": "^2.0.1", 62 | "inquirer": "8.0.0", 63 | "ora": "5.4.1", 64 | "pako": "1.0.6", 65 | "protobufjs": "6.9.0", 66 | "slog-progress": "1.1.4" 67 | }, 68 | "gitHead": "6eb83708dea09759a25fcdc6f5a5587ec0c7cc58" 69 | } 70 | -------------------------------------------------------------------------------- /packages/tiny/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup' 2 | import nodeResolve from '@rollup/plugin-node-resolve' 3 | import babel from '@rollup/plugin-babel' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import { terser } from 'rollup-plugin-terser' 6 | import eslint from '@rollup/plugin-eslint' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | 9 | const extensions = ['.ts', '.tsx'] 10 | 11 | const noDeclarationFiles = { compilerOptions: { declaration: false } } 12 | 13 | // const external = [ 14 | // ...Object.keys(pkg.dependencies || {}), 15 | // ...Object.keys(pkg.peerDependencies || {}) 16 | // ].map(name => RegExp(`^${name}($|/)`)) 17 | 18 | function cjsConfig(prod, type) { 19 | return { 20 | input: 'src/index.ts', 21 | output: { 22 | file: prod 23 | ? `dist/tiny.${type === 'cjs' ? 'cjs' : 'esm'}.prod.js` 24 | : `dist/tiny.${type === 'cjs' ? 'cjs' : 'esm'}.js`, 25 | format: type, 26 | indent: false 27 | }, 28 | // external, 29 | plugins: [ 30 | nodeResolve({ 31 | extensions 32 | }), 33 | commonjs(), 34 | // 将ts声明文件单独提出一份 35 | typescript( 36 | prod && type === 'cjs' 37 | ? { useTsconfigDeclarationDir: true } 38 | : { tsconfigOverride: noDeclarationFiles } 39 | ), 40 | babel({ 41 | extensions, 42 | plugins: [['@babel/plugin-transform-runtime']], 43 | babelHelpers: 'runtime' 44 | }), 45 | eslint({ 46 | throwOnError: true, 47 | throwOnWarning: true, 48 | include: ['src/**'], 49 | exclude: ['node_modules/**'] 50 | }), 51 | prod && terser() 52 | ] 53 | } 54 | } 55 | 56 | function processConfig(prod, type) { 57 | return { 58 | input: 'src/features/process.ts', 59 | output: { 60 | file: 'dist/features/process.js', 61 | format: type, 62 | indent: false 63 | }, 64 | // external, 65 | plugins: [ 66 | nodeResolve({ 67 | extensions 68 | }), 69 | commonjs(), 70 | // 将ts声明文件单独提出一份 71 | typescript( 72 | prod && type === 'cjs' 73 | ? { useTsconfigDeclarationDir: true } 74 | : { tsconfigOverride: noDeclarationFiles } 75 | ), 76 | babel({ 77 | extensions, 78 | plugins: [['@babel/plugin-transform-runtime']], 79 | babelHelpers: 'runtime' 80 | }), 81 | eslint({ 82 | throwOnError: true, 83 | throwOnWarning: true, 84 | include: ['src/**'], 85 | exclude: ['node_modules/**'] 86 | }), 87 | prod && terser() 88 | ] 89 | } 90 | } 91 | 92 | export default defineConfig([ 93 | cjsConfig(true, 'cjs'), 94 | processConfig(true, 'cjs') 95 | ]) 96 | -------------------------------------------------------------------------------- /packages/tiny/src/features/index.ts: -------------------------------------------------------------------------------- 1 | import Https from 'https' 2 | import Url from 'url' 3 | import { DataUploadType, Iheader } from './../interface' 4 | 5 | /** 6 | * 上传函数 7 | * @param { Buffer } file 文件buffer数据 8 | * @returns { Promise } 9 | */ 10 | interface Iupload { 11 | (file: Buffer): Promise 12 | } 13 | export let upload: Iupload 14 | upload = (file: Buffer) => { 15 | const header = randomHeader() 16 | return new Promise((resolve, reject) => { 17 | const req = Https.request(header, res => { 18 | res.on('data', data => { 19 | try { 20 | const resp = JSON.parse(data.toString()) as DataUploadType 21 | if (resp.error) { 22 | reject(resp) 23 | } else { 24 | resolve(resp) 25 | } 26 | } catch (err) { 27 | reject(err) 28 | } 29 | }) 30 | }) 31 | req.write(file) 32 | req.on('error', err => reject(err)) 33 | req.end() 34 | }) 35 | } 36 | 37 | /** 38 | * 下载函数 39 | * @param { string } path 40 | * @returns { Promise } 41 | */ 42 | interface Idownload { 43 | (path: string): Promise 44 | } 45 | export let download: Idownload 46 | download = (path: string) => { 47 | const header = new Url.URL(path) 48 | return new Promise((resolve, reject) => { 49 | const req = Https.request(header, res => { 50 | let content = '' 51 | res.setEncoding('binary') 52 | res.on('data', data => (content += data)) 53 | res.on('end', () => resolve(content)) 54 | }) 55 | req.on('error', err => reject(err)) 56 | req.end() 57 | }) 58 | } 59 | 60 | /** 61 | * 计算byte大小 62 | * @param { number } byte 字节大小 63 | * @returns { string } 64 | */ 65 | interface IbyteSize { 66 | (byte: number): string 67 | } 68 | enum Esize { 69 | B, 70 | KB, 71 | MB, 72 | GB, 73 | TB, 74 | PB, 75 | EB, 76 | ZB, 77 | YB 78 | } 79 | export let byteSize: IbyteSize 80 | byteSize = (byte = 0) => { 81 | if (byte === 0) return '0 B' 82 | const unit = 1024 83 | const i = Math.floor(Math.log(byte) / Math.log(unit)) 84 | return (byte / Math.pow(unit, i)).toPrecision(3) + ' ' + Esize[i] 85 | } 86 | 87 | /** 88 | * 生成随机请求头 89 | * @returns { Iheader } 90 | */ 91 | interface IrandomHeader { 92 | (): Iheader 93 | } 94 | let randomHeader: IrandomHeader 95 | randomHeader = () => { 96 | return { 97 | headers: { 98 | 'Cache-Control': 'no-cache', 99 | 'Content-Type': 'application/x-www-form-urlencoded', 100 | 'Postman-Token': Date.now(), 101 | 'User-Agent': 102 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36', 103 | 'X-Forwarded-For': new Array(4) 104 | .fill(0) 105 | .map(() => parseInt(String(Math.random() * 255), 10)) 106 | .join('.') // 构造ip 107 | }, 108 | hostname: ['tinyjpg.com', 'tinypng.com'][randomNum(0, 1)], // 随机请求 109 | method: 'POST', 110 | path: '/web/shrink', 111 | rejectUnauthorized: false 112 | } 113 | } 114 | 115 | /** 116 | * 生成随机数 117 | * @param { number } min 118 | * @param { number } max 119 | * @returns { number } 120 | */ 121 | interface IrandomNum { 122 | (min?: number, max?: number): number 123 | } 124 | let randomNum: IrandomNum 125 | randomNum = (min = 0, max = 10) => { 126 | return Math.floor(Math.random() * (max - min + 1) + min) 127 | } 128 | 129 | /** 130 | * buffer 转 arraybuffer 131 | * @param { Buffer } 132 | * @returns { ArrayBuffer } 133 | */ 134 | interface ItoArrayBuffer { 135 | (buf: Buffer): ArrayBuffer 136 | } 137 | export let toArrayBuffer: ItoArrayBuffer 138 | toArrayBuffer = (buf: Buffer) => { 139 | const ab = new ArrayBuffer(buf.length) 140 | const view = new Uint8Array(ab) 141 | for (let i = 0; i < buf.length; ++i) { 142 | view[i] = buf[i] 143 | } 144 | return ab 145 | } 146 | 147 | /** 148 | * 转 buffer 149 | * @param { Uint8Array } 150 | * @returns { Buffer } 151 | */ 152 | interface ItoBuffer { 153 | (ab: Uint8Array): Buffer 154 | } 155 | export let toBuffer: ItoBuffer 156 | toBuffer = ab => { 157 | const buf = Buffer.from(ab) 158 | const view = new Uint8Array(ab) 159 | for (let i = 0; i < buf.length; ++i) { 160 | buf[i] = view[i] 161 | } 162 | return buf 163 | } 164 | 165 | // 用于标识该文件是否被压缩过 166 | export const tagBuf = Buffer.from('tiny', 'binary') 167 | export const tagLen = tagBuf.length 168 | 169 | /** 170 | * 转 buffer 171 | * @param { Uint8Array } 172 | * @returns { Buffer } 173 | */ 174 | interface IfilterFileName { 175 | (path: string): string 176 | } 177 | export let filterFileName: IfilterFileName 178 | filterFileName = path => { 179 | let filename: string 180 | if (path.indexOf('/') !== -1) { 181 | filename = path.substring(path.lastIndexOf('/') + 1, path.length) 182 | } else { 183 | filename = path 184 | } 185 | 186 | return filename 187 | } 188 | -------------------------------------------------------------------------------- /packages/tiny/src/features/process.ts: -------------------------------------------------------------------------------- 1 | import { imageType, Idetail, IsvgaData } from './../interface' 2 | import { upload, download, toArrayBuffer, toBuffer, tagBuf } from './index' 3 | import fs from 'fs' 4 | import chalk from 'chalk' 5 | import protobuf from 'protobufjs/light' 6 | import svgaDescriptor from './proto' 7 | 8 | const { assign } = require('pako/lib/utils/common') 9 | const inflate = require('pako/lib/inflate') 10 | const deflate = require('pako/lib/deflate') 11 | const ProtoMovieEntity = protobuf.Root.fromJSON(svgaDescriptor).lookupType( 12 | 'com.opensource.svga.MovieEntity' 13 | ) 14 | const pako: { inflate?: any; deflate?: any } = {} 15 | assign(pako, inflate, deflate) 16 | 17 | /** 18 | * 压缩图片 19 | * @param { imageType } 图片资源 20 | * @returns { promise } 21 | */ 22 | interface IcompressImg { 23 | (payload: imageType): () => Promise 24 | } 25 | let compressImg: IcompressImg 26 | compressImg = ({ path, file }: imageType) => { 27 | return async () => { 28 | const result = { 29 | input: 0, 30 | output: 0, 31 | ratio: 0, 32 | path, 33 | file, 34 | msg: '', 35 | time: 0 36 | } 37 | try { 38 | const start = +new Date() 39 | 40 | // 上传 41 | const dataUpload = await upload(file) 42 | 43 | // 下载 44 | const dataDownload = await download(dataUpload.output.url) 45 | 46 | result.input = dataUpload.input.size 47 | result.output = dataUpload.output.size 48 | result.ratio = 1 - dataUpload.output.ratio 49 | // result.file = Buffer.concat([ 50 | // Buffer.alloc(dataDownload.length, dataDownload, 'binary'), 51 | // tagBuf 52 | // ]) 53 | result.file = Buffer.alloc(dataDownload.length, dataDownload, 'binary') 54 | 55 | result.time = +new Date() - start 56 | } catch (err) { 57 | result.msg = `[${chalk.blue(path)}] ${chalk.red(JSON.stringify(err))}` 58 | } 59 | return result 60 | } 61 | } 62 | 63 | /** 64 | * 压缩svga(图片压缩失败将会被忽略不统计进失败数中) 65 | * @param { string } path 路径 66 | * @param { buffer } source svga buffer 67 | * @returns { promise } 68 | */ 69 | interface IcompressSvga { 70 | (path: string, source: Buffer): () => Promise 71 | } 72 | let compressSvga: IcompressSvga 73 | compressSvga = (path, source) => { 74 | return async () => { 75 | const result = { 76 | input: 0, 77 | output: 0, 78 | ratio: 0, 79 | path, 80 | file: source, 81 | time: 0, 82 | msg: '' 83 | } 84 | try { 85 | const start = +new Date() 86 | 87 | // 解码svga 88 | const data = ProtoMovieEntity.decode( 89 | pako.inflate(toArrayBuffer(source)) 90 | ) as unknown as IsvgaData 91 | const { images } = data 92 | const list = Object.keys(images).map(path => { 93 | return compressImg({ path, file: toBuffer(images[path]) }) 94 | }) 95 | 96 | // 对svga图片进行压缩 97 | const detail = await Promise.all(list.map(fn => fn())) 98 | detail.forEach(({ path, file }) => { 99 | data.images[path] = file 100 | }) 101 | 102 | // 压缩buffer 103 | const file = pako.deflate( 104 | toArrayBuffer(ProtoMovieEntity.encode(data).finish() as Buffer) 105 | ) 106 | result.input = source.length 107 | result.output = file.length 108 | result.ratio = 1 - file.length / source.length 109 | result.file = file 110 | 111 | result.time = +new Date() - start 112 | } catch (err) { 113 | result.msg = `[${chalk.blue(path)}] ${chalk.red(JSON.stringify(err))}` 114 | } 115 | return result 116 | } 117 | } 118 | 119 | /** 120 | * 接收进程任务 121 | */ 122 | process.on('message', (tasks: imageType[]) => { 123 | ;(async () => { 124 | // 优化 png/jpg 125 | const data = tasks 126 | .filter(({ path }: { path: string }) => /\.(jpe?g|png)$/.test(path)) 127 | .map(ele => { 128 | return compressImg({ ...ele, file: Buffer.from(ele.file) }) 129 | }) 130 | 131 | // 优化 svga 132 | const svgaData = tasks 133 | .filter(({ path }: { path: string }) => /\.(svga)$/.test(path)) 134 | .map(ele => { 135 | return compressSvga(ele.path, Buffer.from(ele.file)) 136 | }) 137 | 138 | const details = await Promise.all([ 139 | ...data.map(fn => fn()), 140 | ...svgaData.map(fn => fn()) 141 | ]) 142 | 143 | // 写入 144 | await Promise.all( 145 | details.map( 146 | ({ path, file }) => 147 | new Promise((resolve, reject) => { 148 | fs.writeFile(path, Buffer.concat([file, tagBuf]), err => { 149 | if (err) reject(err) 150 | resolve(true) 151 | }) 152 | }) 153 | ) 154 | ) 155 | 156 | // 发送结果 157 | if (process.send) { 158 | process.send(details) 159 | } 160 | })() 161 | }) 162 | -------------------------------------------------------------------------------- /packages/tiny/src/features/proto.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | nested: { 3 | com: { 4 | nested: { 5 | opensource: { 6 | nested: { 7 | svga: { 8 | options: { 9 | objc_class_prefix: 'SVGAProto', 10 | java_package: 'com.opensource.svgaplayer.proto' 11 | }, 12 | nested: { 13 | MovieParams: { 14 | fields: { 15 | viewBoxWidth: { 16 | type: 'float', 17 | id: 1 18 | }, 19 | viewBoxHeight: { 20 | type: 'float', 21 | id: 2 22 | }, 23 | fps: { 24 | type: 'int32', 25 | id: 3 26 | }, 27 | frames: { 28 | type: 'int32', 29 | id: 4 30 | } 31 | } 32 | }, 33 | SpriteEntity: { 34 | fields: { 35 | imageKey: { 36 | type: 'string', 37 | id: 1 38 | }, 39 | frames: { 40 | rule: 'repeated', 41 | type: 'FrameEntity', 42 | id: 2 43 | }, 44 | matteKey: { 45 | type: 'string', 46 | id: 3 47 | } 48 | } 49 | }, 50 | AudioEntity: { 51 | fields: { 52 | audioKey: { 53 | type: 'string', 54 | id: 1 55 | }, 56 | startFrame: { 57 | type: 'int32', 58 | id: 2 59 | }, 60 | endFrame: { 61 | type: 'int32', 62 | id: 3 63 | }, 64 | startTime: { 65 | type: 'int32', 66 | id: 4 67 | }, 68 | totalTime: { 69 | type: 'int32', 70 | id: 5 71 | } 72 | } 73 | }, 74 | Layout: { 75 | fields: { 76 | x: { 77 | type: 'float', 78 | id: 1 79 | }, 80 | y: { 81 | type: 'float', 82 | id: 2 83 | }, 84 | width: { 85 | type: 'float', 86 | id: 3 87 | }, 88 | height: { 89 | type: 'float', 90 | id: 4 91 | } 92 | } 93 | }, 94 | Transform: { 95 | fields: { 96 | a: { 97 | type: 'float', 98 | id: 1 99 | }, 100 | b: { 101 | type: 'float', 102 | id: 2 103 | }, 104 | c: { 105 | type: 'float', 106 | id: 3 107 | }, 108 | d: { 109 | type: 'float', 110 | id: 4 111 | }, 112 | tx: { 113 | type: 'float', 114 | id: 5 115 | }, 116 | ty: { 117 | type: 'float', 118 | id: 6 119 | } 120 | } 121 | }, 122 | ShapeEntity: { 123 | oneofs: { 124 | args: { 125 | oneof: ['shape', 'rect', 'ellipse'] 126 | } 127 | }, 128 | fields: { 129 | type: { 130 | type: 'ShapeType', 131 | id: 1 132 | }, 133 | shape: { 134 | type: 'ShapeArgs', 135 | id: 2 136 | }, 137 | rect: { 138 | type: 'RectArgs', 139 | id: 3 140 | }, 141 | ellipse: { 142 | type: 'EllipseArgs', 143 | id: 4 144 | }, 145 | styles: { 146 | type: 'ShapeStyle', 147 | id: 10 148 | }, 149 | transform: { 150 | type: 'Transform', 151 | id: 11 152 | } 153 | }, 154 | nested: { 155 | ShapeType: { 156 | values: { 157 | SHAPE: 0, 158 | RECT: 1, 159 | ELLIPSE: 2, 160 | KEEP: 3 161 | } 162 | }, 163 | ShapeArgs: { 164 | fields: { 165 | d: { 166 | type: 'string', 167 | id: 1 168 | } 169 | } 170 | }, 171 | RectArgs: { 172 | fields: { 173 | x: { 174 | type: 'float', 175 | id: 1 176 | }, 177 | y: { 178 | type: 'float', 179 | id: 2 180 | }, 181 | width: { 182 | type: 'float', 183 | id: 3 184 | }, 185 | height: { 186 | type: 'float', 187 | id: 4 188 | }, 189 | cornerRadius: { 190 | type: 'float', 191 | id: 5 192 | } 193 | } 194 | }, 195 | EllipseArgs: { 196 | fields: { 197 | x: { 198 | type: 'float', 199 | id: 1 200 | }, 201 | y: { 202 | type: 'float', 203 | id: 2 204 | }, 205 | radiusX: { 206 | type: 'float', 207 | id: 3 208 | }, 209 | radiusY: { 210 | type: 'float', 211 | id: 4 212 | } 213 | } 214 | }, 215 | ShapeStyle: { 216 | fields: { 217 | fill: { 218 | type: 'RGBAColor', 219 | id: 1 220 | }, 221 | stroke: { 222 | type: 'RGBAColor', 223 | id: 2 224 | }, 225 | strokeWidth: { 226 | type: 'float', 227 | id: 3 228 | }, 229 | lineCap: { 230 | type: 'LineCap', 231 | id: 4 232 | }, 233 | lineJoin: { 234 | type: 'LineJoin', 235 | id: 5 236 | }, 237 | miterLimit: { 238 | type: 'float', 239 | id: 6 240 | }, 241 | lineDashI: { 242 | type: 'float', 243 | id: 7 244 | }, 245 | lineDashII: { 246 | type: 'float', 247 | id: 8 248 | }, 249 | lineDashIII: { 250 | type: 'float', 251 | id: 9 252 | } 253 | }, 254 | nested: { 255 | RGBAColor: { 256 | fields: { 257 | r: { 258 | type: 'float', 259 | id: 1 260 | }, 261 | g: { 262 | type: 'float', 263 | id: 2 264 | }, 265 | b: { 266 | type: 'float', 267 | id: 3 268 | }, 269 | a: { 270 | type: 'float', 271 | id: 4 272 | } 273 | } 274 | }, 275 | LineCap: { 276 | values: { 277 | LineCap_BUTT: 0, 278 | LineCap_ROUND: 1, 279 | LineCap_SQUARE: 2 280 | } 281 | }, 282 | LineJoin: { 283 | values: { 284 | LineJoin_MITER: 0, 285 | LineJoin_ROUND: 1, 286 | LineJoin_BEVEL: 2 287 | } 288 | } 289 | } 290 | } 291 | } 292 | }, 293 | FrameEntity: { 294 | fields: { 295 | alpha: { 296 | type: 'float', 297 | id: 1 298 | }, 299 | layout: { 300 | type: 'Layout', 301 | id: 2 302 | }, 303 | transform: { 304 | type: 'Transform', 305 | id: 3 306 | }, 307 | clipPath: { 308 | type: 'string', 309 | id: 4 310 | }, 311 | shapes: { 312 | rule: 'repeated', 313 | type: 'ShapeEntity', 314 | id: 5 315 | } 316 | } 317 | }, 318 | MovieEntity: { 319 | fields: { 320 | version: { 321 | type: 'string', 322 | id: 1 323 | }, 324 | params: { 325 | type: 'MovieParams', 326 | id: 2 327 | }, 328 | images: { 329 | keyType: 'string', 330 | type: 'bytes', 331 | id: 3 332 | }, 333 | sprites: { 334 | rule: 'repeated', 335 | type: 'SpriteEntity', 336 | id: 4 337 | }, 338 | audios: { 339 | rule: 'repeated', 340 | type: 'AudioEntity', 341 | id: 5 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /packages/tiny/src/index.ts: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer') 2 | import chalk from 'chalk' 3 | import { resolve } from 'path' 4 | import fs from 'fs' 5 | import Ora from 'ora' 6 | import { imageType, Idetail } from './interface' 7 | import Os from 'os' 8 | import { 9 | byteSize, 10 | tagBuf, 11 | tagLen, 12 | toArrayBuffer, 13 | filterFileName 14 | } from './features/index' 15 | import Slogbar from 'slog-progress' 16 | const CG = require('console-grid') 17 | const cluster = require('cluster') 18 | const cpuNums = Os.cpus().length 19 | let bar = new Slogbar('进度 :percent :token :bar :current/:total \n', 50) 20 | let spinner: Ora.Ora // ora载体 21 | let inputSize = 0 // 输入总体积 22 | let outputSize = 0 // 输出总体积 23 | let ratio = 0 // 压缩比 24 | 25 | /** 26 | * 查找目标文件夹 27 | * @param { string } folderName 28 | * @returns { void } 29 | */ 30 | interface IfindFolder { 31 | (folderName: string): void 32 | } 33 | let findFolder: IfindFolder 34 | findFolder = (folderName: string) => { 35 | spinner = Ora(`正在搜索 「${chalk.blueBright(folderName)}」 ......`) 36 | spinner.start() 37 | // 找出所有目标文件夹 38 | const targetFolders = deepFindFolder(resolve(process.cwd()), folderName) 39 | spinner.stop() 40 | if (!targetFolders.length) { 41 | spinner.fail( 42 | `找不到「${chalk.blueBright(folderName)}」,请检查名称是否正确!` 43 | ) 44 | } else { 45 | inquirer 46 | .prompt([ 47 | { 48 | type: 'checkbox', 49 | message: chalk.green('[yx-tiny]') + ' 请选择「目标文件夹」(可多选)?', 50 | choices: targetFolders.map(item => ({ value: item, name: item })), 51 | name: 'folderList', 52 | pageSize: 10 53 | } 54 | ]) 55 | .then(({ folderList }: { folderList: Array }) => { 56 | mapFolder(folderList) 57 | }) 58 | .catch((error: Error) => { 59 | spinner.fail(JSON.stringify(error)) 60 | }) 61 | } 62 | } 63 | 64 | /** 65 | * 递归找出目标文件夹 66 | * @param { string } path 路径 67 | * @param { string } target 目标文件夹 68 | * @returns { Array } 69 | */ 70 | interface IdeepFindFolder { 71 | (path: string, target: string): Array 72 | } 73 | let deepFindFolder: IdeepFindFolder 74 | deepFindFolder = (path: string, target: string) => { 75 | let targetFolders: Array = [] 76 | const ignorePath = ['node_modules', 'dist', '.git'] // 忽略文件 77 | fs.readdirSync(path).forEach((fold: string) => { 78 | const filePath = resolve(path, fold) 79 | const info = fs.statSync(filePath) 80 | if (info.isDirectory() && ignorePath.indexOf(fold) === -1) { 81 | if (fold === target) { 82 | targetFolders.push(filePath) 83 | } else { 84 | // 该对象是文件夹且不为目标文件夹时,递归该对象 85 | targetFolders = [...targetFolders, ...deepFindFolder(filePath, target)] 86 | } 87 | } 88 | }) 89 | return targetFolders 90 | } 91 | 92 | /** 93 | * 遍历处理每个目标文件 94 | * @param { Array } folderList 95 | */ 96 | interface ImapFolder { 97 | (folderList: Array): void 98 | } 99 | let mapFolder: ImapFolder 100 | mapFolder = async (folderList: Array) => { 101 | let target: Array = [] 102 | // 查找目标文件夹内的图片资源 103 | folderList.forEach(path => { 104 | target = [...target, ...deepFindImg(path)] 105 | }) 106 | if (target.length) { 107 | const noCompressList: Array = [] // 未压缩列表 108 | const hasCompressList: Array = [] // 已压缩列表 109 | let len = 0 110 | 111 | while (len < target.length) { 112 | const { path } = target[len] 113 | let data = '' 114 | const curBuf: Buffer = await new Promise((resolve, reject) => { 115 | const readerStream = fs.createReadStream(path) 116 | readerStream.setEncoding('utf8') 117 | readerStream.on('data', chunk => { 118 | data += chunk 119 | }) 120 | readerStream.on('end', () => { 121 | const buf = Buffer.alloc(data.length, data, 'binary') 122 | resolve( 123 | Buffer.from( 124 | toArrayBuffer(buf).slice(buf.length - tagLen, buf.length) 125 | ) 126 | ) 127 | }) 128 | readerStream.on('error', err => { 129 | reject(err.stack) 130 | }) 131 | }) 132 | try { 133 | if (curBuf.compare(tagBuf) !== 0) { 134 | noCompressList.push(target[len]) 135 | } else { 136 | hasCompressList.push(target[len]) 137 | } 138 | } catch (err) { 139 | spinner.fail(`读取 ${path} 资源失败!`) 140 | } 141 | len++ 142 | } 143 | 144 | // 未压缩的svga数量 145 | const noCompressSvgaNum = noCompressList.filter(ele => 146 | /\.(svga)$/.test(ele.path) 147 | ).length 148 | // 未压缩的图片数量 149 | const noCompressImageNum = noCompressList.length - noCompressSvgaNum 150 | 151 | // 已压缩的svga数量 152 | const hasCompressSvgaNum = hasCompressList.filter(ele => 153 | /\.(svga)$/.test(ele.path) 154 | ).length 155 | // 已压缩的图片数量 156 | const hasCompressImageNum = hasCompressList.length - hasCompressSvgaNum 157 | 158 | CG({ 159 | options: { 160 | headerVisible: true 161 | }, 162 | columns: ['类型', '可压缩', '已压缩', '总数'], 163 | rows: [ 164 | [ 165 | '图片', 166 | chalk.red(noCompressImageNum), 167 | chalk.green(hasCompressImageNum), 168 | chalk.blue(noCompressImageNum + hasCompressImageNum) 169 | ], 170 | [ 171 | 'SVGA', 172 | chalk.red(noCompressSvgaNum), 173 | chalk.green(hasCompressSvgaNum), 174 | chalk.blue(noCompressSvgaNum + hasCompressSvgaNum) 175 | ] 176 | ] 177 | }) 178 | 179 | if (!noCompressList.length) { 180 | spinner.fail(`「目标文件夹内」找不到「可压缩」的资源!`) 181 | spinner.stop() 182 | return 183 | } 184 | inquirer 185 | .prompt([ 186 | { 187 | type: 'list', 188 | message: chalk.green('[yx-tiny]') + ' 请选择压缩模式?', 189 | name: 'compressType', 190 | choices: [ 191 | { 192 | value: 'all', 193 | name: '全 量' 194 | }, 195 | { 196 | value: 'diy', 197 | name: '自定义' 198 | } 199 | ], 200 | pageSize: 2 201 | }, 202 | { 203 | type: 'checkbox', 204 | message: chalk.green('[yx-tiny]') + ' 请选择需要压缩的图片?', 205 | name: 'compressList', 206 | choices: target.map(img => ({ value: img, name: img.path })), 207 | pageSize: 10, 208 | when: function ({ compressType }: { compressType: 'diy' | 'all' }) { 209 | return compressType === 'diy' 210 | } 211 | } 212 | ]) 213 | .then( 214 | async ({ 215 | compressType, 216 | compressList 217 | }: { 218 | compressType: 'diy' | 'all' 219 | compressList: Array 220 | }) => { 221 | // 根据用户选择处理对应的资源 222 | const list = compressType == 'all' ? noCompressList : compressList 223 | if (!list.length) { 224 | spinner.fail(`请至少选择一个!`) 225 | spinner.stop() 226 | return 227 | } 228 | 229 | // 开始时间 230 | const dateStart = +new Date() 231 | 232 | cluster.setupPrimary({ 233 | exec: resolve(__dirname, 'features/process.js') 234 | }) 235 | 236 | // 若资源数小于则创建一个进程,否则创建多个进程 237 | const works: Array<{ work: any; tasks: Array }> = [] 238 | if (list.length <= cpuNums) { 239 | works.push({ work: cluster.fork(), tasks: list }) 240 | } else { 241 | for (let i = 0; i < cpuNums; ++i) { 242 | const work = cluster.fork() 243 | works.push({ work, tasks: [] }) 244 | } 245 | } 246 | 247 | // 平均分配任务 248 | let workNum = 0 249 | list.forEach(task => { 250 | if (works.length === 1) { 251 | return 252 | } else if (workNum >= works.length) { 253 | works[0].tasks.push(task) 254 | workNum = 1 255 | } else { 256 | works[workNum].tasks.push(task) 257 | workNum += 1 258 | } 259 | }) 260 | 261 | // 用于记录进程完成数 262 | let pageNum = works.length 263 | let succeedNum = 0 // 成功资源数 264 | let failNum = 0 // 失败资源数 265 | const failMsg: Array = [] // 失败列表 266 | let outputTabel: Idetail[] = [] 267 | // 初始化进度条 268 | bar.render({ 269 | current: 0, 270 | total: list.length, 271 | token: `${chalk.green(0)} 个成功 ${chalk.red(0)} 个失败` 272 | }) 273 | works.forEach(({ work, tasks }) => { 274 | // 发送任务到每个进程 275 | work.send(tasks) 276 | // 接收任务完成 277 | work.on('message', (details: Idetail[]) => { 278 | outputTabel = outputTabel.concat(details) 279 | 280 | // 统计 成功/失败 个数 281 | details.forEach((item: Idetail) => { 282 | if (item.output) { 283 | inputSize += item.input 284 | outputSize += item.output 285 | ratio += item.ratio 286 | succeedNum++ 287 | } else { 288 | failNum++ 289 | if (item.msg) failMsg.push(item.msg) 290 | } 291 | // 更新进度条 292 | bar.render({ 293 | current: succeedNum + failNum, 294 | total: list.length, 295 | token: `${chalk.green(succeedNum)} 个成功 ${chalk.red( 296 | failNum 297 | )} 个失败` 298 | }) 299 | }) 300 | pageNum-- 301 | // 所有任务执行完毕 302 | if (pageNum === 0) { 303 | if (failMsg.length) { 304 | failMsg.forEach(msg => { 305 | spinner.fail(msg) 306 | }) 307 | } 308 | 309 | // 打印表格 310 | CG({ 311 | options: { 312 | headerVisible: true 313 | }, 314 | columns: [ 315 | '名称', 316 | '原体积', 317 | '现体积', 318 | '压缩率', 319 | '耗时', 320 | '状态' 321 | ], 322 | rows: [ 323 | ...outputTabel.map(item => [ 324 | chalk.blue(filterFileName(item.path)), 325 | chalk.red(byteSize(item.input)), 326 | chalk.green(byteSize(item.output)), 327 | !item.ratio 328 | ? chalk.red('0 %') 329 | : chalk.green((item.ratio * 100).toFixed(4) + ' %'), 330 | chalk.cyan(item.time + ' ms'), 331 | item.output ? chalk.green('success') : chalk.red('fail') 332 | ]) 333 | ] 334 | }) 335 | 336 | const totalRatio = ratio / succeedNum 337 | 338 | spinner.succeed( 339 | `资源压缩完成! \n原体积: ${chalk.red( 340 | byteSize(inputSize) 341 | )}\n现体积: ${chalk.green(byteSize(outputSize))}\n压缩率: ${ 342 | totalRatio 343 | ? chalk.green((totalRatio * 100).toFixed(4) + ' %') 344 | : chalk.red('0 %') 345 | }\n成功率: ${chalk.green( 346 | ((succeedNum / list.length) * 100).toFixed(2) + ' %' 347 | )}\n进程数: ${chalk.blue(works.length)}\n总耗时: ${chalk.cyan( 348 | +new Date() - dateStart + ' ms' 349 | )}\n` 350 | ) 351 | 352 | cluster.disconnect() 353 | } 354 | }) 355 | }) 356 | } 357 | ) 358 | } else { 359 | spinner.fail(`找不到可压缩资源!`) 360 | spinner.stop() 361 | } 362 | } 363 | 364 | /** 365 | * 递归找出所有图片 366 | * @param { string } path 367 | * @returns { Array } 368 | */ 369 | interface IdeepFindImg { 370 | (path: string): Array 371 | } 372 | let deepFindImg: IdeepFindImg 373 | deepFindImg = (path: string) => { 374 | const content = fs.readdirSync(path) 375 | let images: Array = [] 376 | content.forEach(folder => { 377 | const filePath = resolve(path, folder) 378 | const info = fs.statSync(filePath) 379 | if (info.isDirectory()) { 380 | images = [...images, ...deepFindImg(filePath)] 381 | } else { 382 | const fileNameReg = /\.(jpe?g|png|svga)$/ 383 | const shouldFormat = fileNameReg.test(filePath) 384 | if (shouldFormat) { 385 | const imgData = fs.readFileSync(filePath) 386 | images.push({ 387 | path: filePath, 388 | file: imgData 389 | }) 390 | } 391 | } 392 | }) 393 | return images 394 | } 395 | 396 | /** 397 | * 程序入口 398 | */ 399 | export default () => { 400 | inquirer 401 | .prompt([ 402 | { 403 | type: 'input', 404 | message: chalk.green('[yx-tiny]') + ' 请输入「文件夹名称」?', 405 | name: 'folderName' 406 | } 407 | ]) 408 | .then(({ folderName }: { folderName: string }) => { 409 | findFolder(folderName) 410 | }) 411 | } 412 | -------------------------------------------------------------------------------- /packages/tiny/src/interface.ts: -------------------------------------------------------------------------------- 1 | export type folderListCheckboxType = { 2 | folderList: string[] 3 | } 4 | 5 | export type compressType = { 6 | compressType: string 7 | compressList: imageType[] 8 | } 9 | 10 | export type imageType = { 11 | path: string 12 | file: Buffer 13 | } 14 | 15 | export type DataUploadType = { 16 | output: { 17 | url: string 18 | size: number 19 | ratio: number 20 | } 21 | input: { 22 | size: number 23 | } 24 | error: string 25 | } 26 | 27 | export type dataDownloadType = { 28 | length: number 29 | } 30 | 31 | export interface Iheader { 32 | headers: { 33 | 'Cache-Control': string 34 | 'Content-Type': string 35 | 'Postman-Token': number 36 | 'User-Agent': string 37 | 'X-Forwarded-For': string 38 | } 39 | hostname: string 40 | method: string 41 | path: string 42 | rejectUnauthorized: boolean 43 | } 44 | 45 | export interface Idetail { 46 | input: number 47 | output: number 48 | ratio: number 49 | path: string 50 | file: Buffer 51 | time?: number 52 | msg?: string 53 | } 54 | 55 | export interface IsvgaData { 56 | images: { 57 | [propsName: string]: Uint8Array 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/tiny/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, // 严格模式 4 | "target": "ESNext", // 指定ECMAScript目标版本(ESNext为最新) 5 | "module": "ESNext", // 指定生成哪个模块系统代码 6 | "moduleResolution": "Node", // 如何处理模块 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, // 跳过.d.ts检测 9 | // "lib": ["dom", "es2017"], // 使用默认注入的库 10 | // "jsx": "react", 11 | "declaration": true, // 生成.d.ts文件 12 | "declarationDir": "./dist/types", // 声明文件创建目录 13 | "sourceMap": true, // 生成相应.map文件 14 | // "removeComments": false, // 删除所有注释(默认为false) 15 | "noUnusedLocals": true, // 不允许不使用已定义的变量 16 | "noUnusedParameters": true, // 不允许不使用的参数 17 | "baseUrl": "./" // 解析非相对模块名的基准目录 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } -------------------------------------------------------------------------------- /scripts/build/config.ts: -------------------------------------------------------------------------------- 1 | import { vPkgDir } from './../utils' 2 | 3 | export const buildConfig = [ 4 | { 5 | type: 'list', 6 | message: '请选择构建模式?', 7 | name: 'sBuildType', 8 | default: 'detail', 9 | choices: [ 10 | { 11 | value: 'detail', 12 | name: '自定义 (自定义构建Module)' 13 | }, 14 | { 15 | value: 'all', 16 | name: '默 认 (构建存在代码改动或当前未被构建过的Module)' 17 | } 18 | ], 19 | pageSize: 2 20 | }, 21 | { 22 | type: 'checkbox', 23 | message: '请选择需要构建的Module(可多选)?', 24 | name: 'vPackages', 25 | choices: vPkgDir.map(module => ({ value: module, name: module })), 26 | when: (answers: { sBuildType: string }): boolean => 27 | answers.sBuildType === 'detail' 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /scripts/build/index.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import chalk from 'chalk' 3 | import { run } from './../utils' 4 | import { buildConfig } from './config' 5 | 6 | /** 7 | * 构建项目 8 | * @param {Object} payload sBuildType(构建模式)、vPackages(项目名称) 9 | * @returns {void} 10 | */ 11 | interface Ibuild { 12 | (payload: { sBuildType: string; vPackages?: Array }): void 13 | } 14 | let build: Ibuild 15 | build = ({ sBuildType, vPackages }) => { 16 | // 默认构建方式 17 | if (sBuildType === 'all') { 18 | run('lerna', ['run', 'build']) 19 | // 自定义构建方式 20 | } else { 21 | vPackages && 22 | vPackages.forEach(async pkg => { 23 | await run('lerna', ['run', 'build', `--scope=yx-${pkg}`]) 24 | }) 25 | } 26 | } 27 | 28 | inquirer 29 | .prompt(buildConfig) 30 | .then(answers => { 31 | build({ ...answers }) 32 | }) 33 | .catch(error => { 34 | console.log(chalk.red(`[ERROR] ${JSON.stringify(error)}`)) 35 | }) 36 | -------------------------------------------------------------------------------- /scripts/create/config.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | /** 4 | * kebab-case 转 camelCase 5 | * @param {string} sName 名称 6 | * @returns {string} 7 | */ 8 | interface ItoCamel { 9 | (sName: string): string 10 | } 11 | export let toCamel: ItoCamel 12 | toCamel = (sName: string) => { 13 | return sName.replace(/\-(\w)/g, function (all: string, letter: string) { 14 | return letter.toUpperCase() 15 | }) 16 | } 17 | 18 | interface ImoduleValidate { 19 | (answer: string): boolean 20 | } 21 | let moduleValidate: ImoduleValidate 22 | moduleValidate = (answer: string) => { 23 | const regTest = 24 | /^([^\x00-\xff]|[a-zA-Z_$])([^\x00-\xff]|[a-zA-Z0-9_$])*$/.test(answer) 25 | if (!regTest) { 26 | console.log(chalk.red(`[ERROR] Module名称不符合规范!`)) 27 | return false 28 | } 29 | return true 30 | } 31 | 32 | export const createConfig = [ 33 | { 34 | type: 'input', 35 | message: '请输入 Module 名称', 36 | name: 'sModule', 37 | validate: moduleValidate, 38 | filter: toCamel 39 | }, 40 | { 41 | type: 'input', 42 | message: '请输入 Module 描述', 43 | name: 'sDescription', 44 | default: 'Module created by lerna-npm' 45 | }, 46 | { 47 | type: 'input', 48 | message: '请输入 author 名称', 49 | name: 'sName', 50 | default: 'lerna-npm' 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /scripts/create/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import fs from 'fs' 3 | import Metalsmith from 'metalsmith' 4 | import Handlebars from 'handlebars' 5 | import rimraf from 'rimraf' 6 | import inquirer from 'inquirer' 7 | import chalk from 'chalk' 8 | import { toCamel, createConfig } from './config' 9 | 10 | const sPkgPath = resolve('.', 'packages') // 目标文件夹 11 | const sTempPath = resolve('.', 'scripts', 'temp') // 模板位置 12 | /** 13 | * 创建文件夹 14 | * @param {string} sFoldName 文件夹名称 15 | * @returns {string|false} 16 | */ 17 | interface IcreateFold { 18 | (sFoldName: string): string | false 19 | } 20 | let createFold: IcreateFold 21 | createFold = (sFoldName: string) => { 22 | const sFoldPath = resolve(sPkgPath, sFoldName) 23 | if (!fs.existsSync(sFoldPath)) { 24 | fs.mkdirSync(sFoldPath) 25 | return sFoldPath 26 | } 27 | console.log( 28 | chalk.red(`[ERROR] The "packages/${sFoldName}" fold has existed!`) 29 | ) 30 | return false 31 | } 32 | 33 | /** 34 | * 拉取模板,生成目标项目 35 | * @param {string} sDestpath 文件夹路径 36 | * @param {string} sModule 模块名 37 | * @param {string} sDescription 模块描述 38 | * @param {string} sName 作者名称 39 | * @returns {Promise} 40 | */ 41 | interface IpullLocalTemp { 42 | ( 43 | sDestpath: string, 44 | sModule: string, 45 | sDescription: string, 46 | sName: string 47 | ): Promise 48 | } 49 | let pullLocalTemp: IpullLocalTemp 50 | pullLocalTemp = ( 51 | sDestpath: string, 52 | sModule: string, 53 | sDescription: string, 54 | sName: string 55 | ) => { 56 | return new Promise((resolve, reject) => { 57 | const metadata = { 58 | pkgName: sModule, 59 | pkgCamelName: toCamel(sModule), 60 | description: sDescription, 61 | name: sName 62 | } 63 | // 把文件转换为js对象 64 | Metalsmith(__dirname) 65 | .metadata(metadata) // 需要替换的数据 66 | .source(sTempPath) // 模板位置 67 | .destination(sDestpath) // 目标位置 68 | .use((files, metalsmith, done) => { 69 | // 遍历需要替换模板 70 | Object.keys(files).forEach(fileName => { 71 | // 需先转换为字符串 72 | const fileContentsString = files[fileName].contents.toString() 73 | // 重写文件内容 74 | files[fileName].contents = Buffer.from( 75 | // 使用定义的metaData取代模板变量 76 | Handlebars.compile(fileContentsString)(metalsmith.metadata()) 77 | ) 78 | }) 79 | done(null, files, metalsmith) 80 | }) 81 | .build(function (err) { 82 | if (err) { 83 | console.log(chalk.red(`[ERROR] Metalsmith build error!`)) 84 | reject(false) 85 | throw err 86 | } 87 | resolve(true) 88 | }) 89 | }) 90 | } 91 | 92 | /** 93 | * 程序入口 94 | * @param {object} payload sModule(模块名)、sDescription(模块描述)、sName(作者名称) 95 | * @returns {void} 96 | */ 97 | interface Ientry { 98 | (payload: { sModule: string; sDescription: string; sName: string }): void 99 | } 100 | let entry: Ientry 101 | entry = ({ sModule, sDescription, sName }) => { 102 | if (!sModule) { 103 | console.log(chalk.red(`[ERROR] The package name can not be empty!`)) 104 | return 105 | } 106 | console.log(chalk.blue(`[INFO] Start creating "${sModule}"...`)) 107 | const foldPath = createFold(sModule) 108 | if (!foldPath) return 109 | pullLocalTemp(foldPath, sModule, sDescription, sName) 110 | .then(() => { 111 | console.log( 112 | chalk.green( 113 | `[SUCCESS] Congratulations! The "${sModule}" create successfully!` 114 | ) 115 | ) 116 | }) 117 | .catch(() => { 118 | console.log(chalk.red(`[ERROR] Sorry! The "${sModule}" create failed!`)) 119 | // 删除创建失败的项目 120 | rimraf(foldPath, () => { 121 | console.log(chalk.blue(`[INFO] Delete "${sModule}" package fold!`)) 122 | }) 123 | }) 124 | } 125 | 126 | inquirer 127 | .prompt(createConfig) 128 | .then(answers => { 129 | entry({ ...answers }) 130 | }) 131 | .catch(() => { 132 | console.log(chalk.red(`[ERROR] Inquirer error`)) 133 | }) 134 | -------------------------------------------------------------------------------- /scripts/link/config.ts: -------------------------------------------------------------------------------- 1 | import { vPkgDir } from './../utils' 2 | const vPkgDirList = vPkgDir.map(module => ({ 3 | value: module, 4 | name: module 5 | })) 6 | const iPkgDirLen = vPkgDirList.length 7 | interface Iwhen { 8 | (answer: { sInstallType: string }): boolean 9 | } 10 | let when: Iwhen 11 | when = (answers: { sInstallType: string }): boolean => 12 | answers.sInstallType === 'add' 13 | export const linkConfig = [ 14 | { 15 | type: 'list', 16 | message: '请选择安装模式?', 17 | name: 'sInstallType', 18 | default: 'all', 19 | choices: [ 20 | { 21 | value: 'all', 22 | name: '默 认 (安装所有Module的依赖)' 23 | }, 24 | { 25 | value: 'add', 26 | name: '自定义 (连接其他Module或安装第三方库)' 27 | } 28 | ], 29 | pageSize: 2 30 | }, 31 | { 32 | type: 'list', 33 | message: '请选择目标 Module ?', 34 | name: 'sTargetModule', 35 | choices: vPkgDirList, 36 | pageSize: iPkgDirLen, 37 | when 38 | }, 39 | { 40 | type: 'input', 41 | message: '请输入需要安装的 Module 名称?', 42 | name: 'sInstallModule', 43 | when 44 | }, 45 | { 46 | type: 'list', 47 | message: '请选择相关设置?', 48 | name: 'sOption', 49 | choices: [ 50 | { 51 | value: 'normal', 52 | name: 'dependencies' 53 | }, 54 | { 55 | value: 'dev', 56 | name: 'devDependencies' 57 | }, 58 | { 59 | value: 'peer', 60 | name: 'peerDependencies' 61 | } 62 | ], 63 | pageSize: 3, 64 | when 65 | } 66 | ] 67 | 68 | export const unlinkConfig = [ 69 | { 70 | type: 'list', 71 | message: '请选择目标 Module ?', 72 | name: 'sTargetModule', 73 | choices: vPkgDirList, 74 | pageSize: iPkgDirLen 75 | }, 76 | { 77 | type: 'input', 78 | message: '请输入需要卸载的 Module ?', 79 | name: 'sDelModule' 80 | } 81 | ] 82 | -------------------------------------------------------------------------------- /scripts/link/link.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import chalk from 'chalk' 3 | import { run } from './../utils' 4 | import { linkConfig } from './config' 5 | 6 | /** 7 | * 安装依赖 8 | * @param {Object} payload sInstallType(安装模式)、sInstallModule(依赖名称)、sTargetModule(项目名称)、sOption(安装位置) 9 | * @returns {void} 10 | */ 11 | interface Iinstall { 12 | (payload: { 13 | sInstallType: string 14 | sInstallModule?: string 15 | sTargetModule?: string 16 | sOption?: string 17 | }): void 18 | } 19 | let install: Iinstall 20 | install = ({ sInstallType, sInstallModule, sTargetModule, sOption }) => { 21 | // 一键安装 22 | if (sInstallType === 'all') { 23 | run('lerna', ['bootstrap', '--hoist']) 24 | // 自定义安装 25 | } else { 26 | run( 27 | 'lerna', 28 | ['add', sInstallModule || '', `--scope=yx-${sTargetModule}`].concat( 29 | sOption === 'normal' ? [] : [`--${sOption}`] 30 | ) 31 | ) 32 | } 33 | } 34 | 35 | inquirer 36 | .prompt(linkConfig) 37 | .then(answers => { 38 | install({ ...answers }) 39 | }) 40 | .catch(() => { 41 | console.log(chalk.red(`[ERROR] Inquirer error`)) 42 | }) 43 | -------------------------------------------------------------------------------- /scripts/link/unlink.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import chalk from 'chalk' 3 | import { run } from './../utils' 4 | import { unlinkConfig } from './config' 5 | 6 | /** 7 | * 卸载依赖 8 | * @param {Object} payload sTargetModule(目标项目)、sDelModule(卸载依赖名称) 9 | * @returns {void} 10 | */ 11 | interface Iuninstall { 12 | (payload: { sTargetModule: string; sDelModule: string }): void 13 | } 14 | let uninstall: Iuninstall 15 | uninstall = ({ sTargetModule, sDelModule }) => { 16 | run('lerna', [ 17 | 'exec', 18 | `--scope=yx-${sTargetModule}`, 19 | `npm uninstall ${sDelModule}` 20 | ]) 21 | } 22 | 23 | inquirer 24 | .prompt(unlinkConfig) 25 | .then(answers => { 26 | uninstall({ ...answers }) 27 | }) 28 | .catch(error => { 29 | console.log(chalk.red(`[ERROR] ${JSON.stringify(error)}`)) 30 | }) 31 | -------------------------------------------------------------------------------- /scripts/temp/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/typescript', 4 | // '@babel/preset-typescript', 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: { 9 | browsers: ['ie >= 11'] 10 | }, 11 | // exclude: ['transform-async-to-generator', 'transform-regenerator'], 12 | modules: false // 不用转化esm 13 | // loose: true // 不启用松散模式 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/temp/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /scripts/temp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | 4 | plugins: ['@typescript-eslint'], 5 | 6 | rules: { 7 | 'no-unused-vars': 'off', 8 | '@typescript-eslint/no-unused-expressions': 'off', 9 | '@typescript-eslint/no-unused-vars': [ 10 | 'off', 11 | { 12 | vars: 'all', 13 | args: 'after-used', 14 | ignoreRestSiblings: true, 15 | argsIgnorePattern: '^_', 16 | varsIgnorePattern: '^_' 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/temp/.npmignore: -------------------------------------------------------------------------------- 1 | __test__ 2 | node_mudules 3 | src 4 | test 5 | .babelrc.js 6 | .commitlintrc.js 7 | .editorconfig 8 | .eslintrc.js 9 | .eslintignore 10 | .gitignore 11 | .npmrc 12 | .prettierrc.json 13 | jest.config.js 14 | package-lock.json 15 | tsconfig.json 16 | rollup.config.js -------------------------------------------------------------------------------- /scripts/temp/README.md: -------------------------------------------------------------------------------- 1 | ## {{pkgName}} 2 | 3 | {{description}} -------------------------------------------------------------------------------- /scripts/temp/__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { sum } from '../src/index' 2 | 3 | describe('sum ', () => { 4 | test('This is sum test', () => { 5 | expect(sum(1, 2)).toBe(3) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /scripts/temp/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | coverageProvider: 'v8', 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: './tsconfig.json' 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/temp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yx-{{pkgName}}", 3 | "version": "0.0.0", 4 | "description": "{{description}}", 5 | "keywords": [ 6 | "npm", 7 | "yx", 8 | "{{pkgName}}" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/yxichan/lerna-npm.git" 13 | }, 14 | "license": "ISC", 15 | "author": "{{name}}", 16 | "main": "dist/{{pkgName}}.cjs.prod.js", 17 | "module": "dist/{{pkgName}}.esm.prod.js", 18 | "types": "dist/types/index.d.ts", 19 | "scripts": { 20 | "clean": "rimraf dist", 21 | "lint": "eslint src/* --ext .js,.ts", 22 | "test": "jest", 23 | "prettier": "prettier --write src", 24 | "build": "npm run test && npm run prettier && npm run lint && npm run clean && rollup -c" 25 | }, 26 | "devDependencies": { 27 | "@babel/plugin-transform-runtime": "^7.18.9", 28 | "@babel/preset-env": "^7.18.9", 29 | "@babel/preset-typescript": "^7.18.6", 30 | "@rollup/plugin-babel": "^5.3.1", 31 | "@rollup/plugin-commonjs": "^22.0.1", 32 | "@rollup/plugin-eslint": "^8.0.2", 33 | "@rollup/plugin-node-resolve": "^13.3.0", 34 | "@types/jest": "^28.1.6", 35 | "@typescript-eslint/eslint-plugin": "^5.30.7", 36 | "@typescript-eslint/parser": "^5.30.7", 37 | "eslint": "^8.20.0", 38 | "jest": "^28.1.3", 39 | "jest-environment-jsdom": "^28.1.3", 40 | "prettier": "^2.7.1", 41 | "rollup": "^2.64.0", 42 | "rollup-plugin-terser": "^7.0.2", 43 | "rollup-plugin-typescript2": "^0.32.1", 44 | "ts-jest": "^28.0.7", 45 | "tslib": "^2.4.0", 46 | "typescript": "^4.7.4" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 51 | } 52 | }, 53 | "dependencies": { 54 | "@babel/runtime": "^7.18.9" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/temp/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup' 2 | import nodeResolve from '@rollup/plugin-node-resolve' 3 | import babel from '@rollup/plugin-babel' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import { terser } from 'rollup-plugin-terser' 6 | import eslint from '@rollup/plugin-eslint' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | 9 | const extensions = ['.ts', '.tsx'] 10 | 11 | const noDeclarationFiles = { compilerOptions: { declaration: false } } 12 | 13 | function cjsConfig(prod, type) { 14 | return { 15 | input: 'src/index.ts', 16 | output: { 17 | file: prod 18 | ? `dist/{{pkgName}}.${type === 'cjs' ? 'cjs' : 'esm'}.prod.js` 19 | : `dist/{{pkgName}}.${type === 'cjs' ? 'cjs' : 'esm'}.js`, 20 | format: type, 21 | indent: false 22 | }, 23 | // external, 24 | plugins: [ 25 | nodeResolve({ 26 | extensions 27 | }), 28 | commonjs(), 29 | // 将ts声明文件单独提出一份 30 | typescript( 31 | prod && type === 'cjs' 32 | ? { useTsconfigDeclarationDir: true } 33 | : { tsconfigOverride: noDeclarationFiles } 34 | ), 35 | babel({ 36 | extensions, 37 | plugins: [['@babel/plugin-transform-runtime']], 38 | babelHelpers: 'runtime' 39 | }), 40 | eslint({ 41 | throwOnError: true, 42 | throwOnWarning: true, 43 | include: ['src/**'], 44 | exclude: ['node_modules/**'] 45 | }), 46 | prod && terser() 47 | ] 48 | } 49 | } 50 | 51 | export default defineConfig([ 52 | cjsConfig(false, 'cjs'), 53 | cjsConfig(true, 'cjs'), 54 | cjsConfig(false, 'es'), 55 | cjsConfig(true, 'es') 56 | ]) 57 | -------------------------------------------------------------------------------- /scripts/temp/src/features/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 打印 3 | * @return {string} val 打印内容 4 | */ 5 | interface Isay { 6 | (val: string): void 7 | } 8 | export let say: Isay 9 | say = (val: string) => { 10 | console.log(val) 11 | } 12 | -------------------------------------------------------------------------------- /scripts/temp/src/index.ts: -------------------------------------------------------------------------------- 1 | export { say } from './features' 2 | -------------------------------------------------------------------------------- /scripts/temp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, // 严格模式 4 | "target": "ESNext", // 指定ECMAScript目标版本(ESNext为最新) 5 | "module": "ESNext", // 指定生成哪个模块系统代码 6 | "moduleResolution": "Node", // 如何处理模块 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, // 跳过.d.ts检测 9 | // "lib": ["dom", "es2017"], // 使用默认注入的库 10 | // "jsx": "react", 11 | "declaration": true, // 生成.d.ts文件 12 | "declarationDir": "./dist/types", // 声明文件创建目录 13 | "sourceMap": true, // 生成相应.map文件 14 | // "removeComments": false, // 删除所有注释(默认为false) 15 | "noUnusedLocals": true, // 不允许不使用已定义的变量 16 | "noUnusedParameters": true, // 不允许不使用的参数 17 | "baseUrl": "./" // 解析非相对模块名的基准目录 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } -------------------------------------------------------------------------------- /scripts/test/config.ts: -------------------------------------------------------------------------------- 1 | import { vPkgDir } from './../utils' 2 | 3 | export const testConfig = [ 4 | { 5 | type: 'list', 6 | message: '请选择测试模式?', 7 | name: 'sTestType', 8 | default: 'all', 9 | choices: [ 10 | { 11 | value: 'all', 12 | name: '默 认 (执行packages下的所有Module的测试)' 13 | }, 14 | { 15 | value: 'single', 16 | name: '自定义 (执行指定Module的测试)' 17 | } 18 | ], 19 | pageSize: 2 20 | }, 21 | { 22 | type: 'list', 23 | message: '请选择需要测试 Module ?', 24 | name: 'sTargetModule', 25 | choices: vPkgDir.map(module => ({ value: module, name: module })), 26 | pageSize: vPkgDir.length, 27 | when: (answers: { sTestType: string }): boolean => 28 | answers.sTestType === 'single' 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /scripts/test/index.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import chalk from 'chalk' 3 | import { run } from './../utils' 4 | import { testConfig } from './config' 5 | 6 | /** 7 | * 测试项目 8 | * @param {Object} payload sTestType(测试模式)、sTargetModule(目标项目) 9 | * @returns {void} 10 | */ 11 | interface Itest { 12 | (payload: { sTestType: string; sTargetModule?: string }): void 13 | } 14 | let test: Itest 15 | test = ({ sTestType, sTargetModule }) => { 16 | // 默认版本控制方式 17 | if (sTestType === 'all') { 18 | run('lerna', ['run', 'test', '--no-sort']) 19 | // 自定义版本控制方式 20 | } else { 21 | run('lerna', ['run', 'test', `--scope=yx-${sTargetModule}`]) 22 | } 23 | } 24 | 25 | inquirer 26 | .prompt(testConfig) 27 | .then(answers => { 28 | test({ ...answers }) 29 | }) 30 | .catch(error => { 31 | console.log(chalk.red(`[ERROR] ${JSON.stringify(error)}`)) 32 | }) 33 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import fs from 'fs' 3 | import { resolve } from 'path' 4 | export const vPkgDir = fs 5 | .readdirSync(resolve('.', 'packages')) 6 | .filter(path => !path.includes('.DS_Store')) 7 | export const run = (bin: string, args: string[], opts: any = {}): void => { 8 | execa(bin, args, { stdio: 'inherit', ...opts }) 9 | } 10 | --------------------------------------------------------------------------------