├── .nvmrc ├── .gitattributes ├── examples ├── biz-components │ ├── index.ts │ ├── utils │ │ ├── index.ts │ │ ├── test.ts │ │ └── pages-test.ts │ ├── components │ │ ├── biz-test │ │ │ └── biz-test.vue │ │ └── biz-use-test │ │ │ └── biz-use-test.vue │ ├── tsconfig.json │ └── package.json ├── vue3+vite+ts │ ├── src │ │ ├── types │ │ │ └── vite.d.ts │ │ ├── api │ │ │ ├── test.d.ts │ │ │ ├── test.js │ │ │ └── index.ts │ │ ├── assets │ │ │ └── logo.png │ │ ├── App.vue │ │ ├── lib │ │ │ ├── ajax-grpc.ts │ │ │ ├── pages.ts │ │ │ ├── demo.ts │ │ │ └── base-request.ts │ │ ├── pages-sub-demo │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── components │ │ │ │ └── demo1.vue │ │ │ └── index.vue │ │ ├── pages-sub-async │ │ │ ├── component.vue │ │ │ ├── plugin.ts │ │ │ └── index.vue │ │ ├── main.ts │ │ ├── pages.json │ │ ├── pages │ │ │ └── index.vue │ │ └── manifest.json │ ├── .gitignore │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ └── vite.config.ts ├── hbx+vue3 │ ├── static │ │ └── logo.png │ ├── pages-sub │ │ ├── plugins │ │ │ └── index.js │ │ ├── api │ │ │ └── index.js │ │ └── index │ │ │ └── index.vue │ ├── package.json │ ├── api │ │ └── index.js │ ├── lib │ │ └── base-request.js │ ├── App.vue │ ├── uni.promisify.adaptor.js │ ├── main.js │ ├── types │ │ ├── async-component.d.ts │ │ └── async-import.d.ts │ ├── pages.json │ ├── index.html │ ├── vite.config.js │ ├── pages │ │ └── index │ │ │ └── index.vue │ ├── uni.scss │ └── manifest.json └── common-error-http-sdk │ ├── lixin │ └── common │ │ └── error │ │ └── v1 │ │ ├── rpc.ts │ │ ├── error.enum.ts │ │ └── error.ts │ └── package.json ├── src ├── utils │ ├── query-string │ │ ├── index.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── parse.ts │ ├── vite │ │ ├── index.ts │ │ └── path-resolver.ts │ ├── crypto │ │ ├── readme │ │ ├── index.ts │ │ ├── xxhash.ts │ │ └── base_encode.ts │ ├── visualizer │ │ ├── index.ts │ │ ├── type │ │ │ ├── index.ts │ │ │ ├── base.link.ts │ │ │ ├── base.node.ts │ │ │ ├── base.restrict.ts │ │ │ └── vite.ts │ │ ├── README.md │ │ ├── helper.ts │ │ ├── file-system.ts │ │ └── transform.ts │ ├── lex-parse │ │ ├── readme │ │ ├── index.ts │ │ ├── type.d.ts │ │ ├── parse_import.ts │ │ └── parse_arguments.ts │ ├── nunjucks │ │ └── index.ts │ ├── base64url │ │ ├── pad-string.ts │ │ └── index.ts │ ├── regexp │ │ └── index.ts │ ├── segment-iterator │ │ ├── type.ts │ │ └── index.ts │ ├── split-on-first │ │ └── index.ts │ ├── decode-uri-component │ │ └── index.ts │ ├── uniapp │ │ └── index.ts │ └── index.ts ├── env.d.ts ├── common │ ├── AsyncImports.ts │ ├── AsyncComponents.ts │ ├── ParseOptions.ts │ └── Logger.ts ├── constants.ts ├── index.ts ├── plugin │ ├── vite-plugin-dep-graph.ts │ ├── vite-plugin-global-method.ts │ ├── async-import-processor.ts │ ├── async-component-processor.ts │ ├── template.njk │ └── subpackages-optimization.ts └── type.d.ts ├── .gitignore ├── eslint.config.ts ├── .vscode ├── schema │ ├── commitlint-with-cz.json │ ├── readme.md │ ├── commitlint-patch.json │ └── commitlint.json └── settings.json ├── tsconfig.json ├── pnpm-workspace.yaml ├── .github ├── workflows │ ├── pkg.pr.new.yml │ ├── deploy.yml │ └── release.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── LICENSE ├── package.json ├── .commitlintrc.yaml └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /examples/biz-components/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/utils/query-string/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parse' 2 | -------------------------------------------------------------------------------- /src/utils/vite/index.ts: -------------------------------------------------------------------------------- 1 | export * from './path-resolver' 2 | -------------------------------------------------------------------------------- /examples/biz-components/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test' 2 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/types/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/utils/crypto/readme: -------------------------------------------------------------------------------- 1 | > 本模块主要是对代码字符串hash化的一些工具函数 2 | > 3 | > 部分内容是相关rust库的转写 4 | > 5 | > 暂时没用上 -------------------------------------------------------------------------------- /examples/vue3+vite+ts/.gitignore: -------------------------------------------------------------------------------- 1 | async-component.d.ts 2 | async-import.d.ts 3 | components.d.ts 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build Outputs 5 | build 6 | dist 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/api/test.d.ts: -------------------------------------------------------------------------------- 1 | declare function demo(msg: string): void; 2 | export default demo; 3 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/api/test.js: -------------------------------------------------------------------------------- 1 | export default function demo(msg) { 2 | console.log('[demo-func]', msg); 3 | } -------------------------------------------------------------------------------- /src/utils/visualizer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file-system' 2 | export * from './transform' 3 | export * from './type' 4 | -------------------------------------------------------------------------------- /examples/hbx+vue3/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uni-ku/bundle-optimizer/HEAD/examples/hbx+vue3/static/logo.png -------------------------------------------------------------------------------- /src/utils/lex-parse/readme: -------------------------------------------------------------------------------- 1 | # lex-parse 2 | 3 | > 实现了一个简单的词法分析器,用于解析代码字符串中的函数调用的场景 4 | > 需要传入代码字符串和需要解析的函数名,返回函数调用的位置信息和参数信息的位置信息 -------------------------------------------------------------------------------- /examples/hbx+vue3/pages-sub/plugins/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test(msg) { 3 | console.log(msg, 'plugin:test') 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uni-ku/bundle-optimizer/HEAD/examples/vue3+vite+ts/src/assets/logo.png -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/utils/visualizer/type/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.link' 2 | export * from './base.node' 3 | export * from './base.restrict' 4 | export * from './vite' 5 | -------------------------------------------------------------------------------- /examples/biz-components/utils/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export function testUtil(params: any) { 3 | console.log('testUtil called with params:', params) 4 | } 5 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | ignores: [ 5 | 'examples/hbx+vue3', 6 | 'examples/vue3+vite+ts', 7 | ], 8 | }) 9 | -------------------------------------------------------------------------------- /examples/hbx+vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lodash": "catalog:" 4 | }, 5 | "devDependencies": { 6 | "@uni-ku/bundle-optimizer": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/hbx+vue3/api/index.js: -------------------------------------------------------------------------------- 1 | import { getRequest } from '@/lib/base-request' 2 | 3 | export function getMainPackageTestApi(params) { 4 | return getRequest('/main-package-test', params) 5 | } 6 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages-sub/api/index.js: -------------------------------------------------------------------------------- 1 | import { getRequest } from '@/lib/base-request' 2 | 3 | export function getSubPackageTestApi(params) { 4 | return getRequest('/sub-package-test', params) 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { getRequest } from '@/lib/base-request' 2 | 3 | export function getMainPackageTestApi(params?: any) { 4 | return getRequest('/main-package-test', params) 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/lib/ajax-grpc.ts: -------------------------------------------------------------------------------- 1 | // import { ErrorReasonStrValue } from "@lixin-sdk/common-error-http-sdk/lixin/common/error/v1/error.enum"; 2 | 3 | export function getErrorReasonStrValue() { 4 | return {}; 5 | } -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-demo/api/index.ts: -------------------------------------------------------------------------------- 1 | import { getRequest } from '@/lib/base-request' 2 | 3 | export function getSubPackageTestApi(params: any) { 4 | return getRequest('/sub-package-test', params) 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-async/component.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/lib/pages.ts: -------------------------------------------------------------------------------- 1 | import { pages, subPackages } from '@/pages.json' 2 | 3 | export function getPages() { 4 | return pages 5 | } 6 | 7 | export function getSubPackages() { 8 | return subPackages 9 | } 10 | -------------------------------------------------------------------------------- /examples/biz-components/utils/pages-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // @ts-expect-error ignore 3 | import e from '@/pages.json' 4 | 5 | export default { 6 | test: () => { 7 | console.log('pages-test', e) 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from 'vue' 2 | import App from './App.vue' 3 | import demo from './api/test' 4 | 5 | demo('entry') 6 | 7 | export function createApp() { 8 | const app = createSSRApp(App) 9 | 10 | return { 11 | app, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/visualizer/type/base.link.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 节点连接关系 3 | */ 4 | export interface GraphLink { 5 | /** 依赖来源方的 id */ 6 | source: string 7 | 8 | /** 被依赖方的 id */ 9 | target: string 10 | 11 | /** 12 | * 边的类型 13 | */ 14 | type: T 15 | } 16 | -------------------------------------------------------------------------------- /examples/hbx+vue3/lib/base-request.js: -------------------------------------------------------------------------------- 1 | export function getRequest(path, params) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | console.log('[api:get]', { path, params }) 5 | resolve({ 6 | path, 7 | params, 8 | }) 9 | }, 1000) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/lib/demo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/no-namespace */ 2 | import { styleText } from "@https-enable/colors" 3 | 4 | export namespace MathUtils { 5 | export const add = (a: number, b: number) => { 6 | return styleText(["bgBrightYellow", "underline", "cyan"], `${a} + ${b} = ${a + b}`) 7 | } 8 | } -------------------------------------------------------------------------------- /examples/biz-components/components/biz-test/biz-test.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/lex-parse/index.ts: -------------------------------------------------------------------------------- 1 | import { lexFunctionCalls } from './parse_arguments' 2 | 3 | export function parseAsyncImports(source: string) { 4 | return lexFunctionCalls(source, 'AsyncImport') 5 | } 6 | 7 | export * from './parse_arguments' 8 | export * from './parse_import' 9 | export type * from './type' 10 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | /** `process.env.[xxx]` 只能赋值字符串 */ 3 | interface ProcessEnv { 4 | UNI_PLATFORM?: string 5 | UNI_INPUT_DIR?: string 6 | UNI_OPT_TRACE?: string 7 | } 8 | 9 | /** 只有在 `process.[xxx]` 才可以赋值复杂对象 */ 10 | interface Process { 11 | UNI_SUBPACKAGES?: any 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/hbx+vue3/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /.vscode/schema/commitlint-with-cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "type": "object", 4 | "allOf": [ 5 | { "$ref": "commitlint-patch.json" } 6 | ], 7 | "properties": { 8 | "prompt": { 9 | "description": "Prompt settings (git-cz)", 10 | "$ref": "cz-git.json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/biz-components/components/biz-use-test/biz-use-test.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/lib/base-request.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export function getRequest(path: string, params: any) { 3 | return new Promise((resolve) => { 4 | setTimeout(() => { 5 | console.log('[api:get]', { path, params }) 6 | resolve({ 7 | path, 8 | params, 9 | }) 10 | }, 1000) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /examples/common-error-http-sdk/lixin/common/error/v1/rpc.ts: -------------------------------------------------------------------------------- 1 | export const protobufPackage = 'lixin.common.error.v1' 2 | 3 | /** rpc 方法错误声明 */ 4 | export interface MethodDescriptor { 5 | /** rpc方法返回的所有错误 */ 6 | errors: MethodDescriptor_Item[] 7 | } 8 | 9 | /** 错误条目 */ 10 | export interface MethodDescriptor_Item { 11 | /** rpc方法返回的错误 */ 12 | error: string 13 | /** 错误描述 */ 14 | remark: string 15 | } 16 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-async/plugin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export function AsyncPluginDemo() { 3 | return { 4 | name: 'async-plugin', 5 | run() { 6 | console.log('[async-plugin]', 'run') 7 | uni.showToast({ 8 | title: '异步插件执行✨', 9 | mask: true, 10 | icon: 'success', 11 | }) 12 | }, 13 | } 14 | } 15 | 16 | export default AsyncPluginDemo 17 | -------------------------------------------------------------------------------- /src/utils/visualizer/type/base.node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础节点 3 | */ 4 | export interface GraphBaseNode { 5 | /** 唯一标识符,直接使用 Rollup 的 module.id */ 6 | id: string 7 | /** 简短文本 */ 8 | name: string 9 | /** 业务需求的显示文本 */ 10 | label?: string 11 | /** 节点类型 */ 12 | type: T 13 | /** 14 | * 节点的权重值 15 | * @TODO: 权重值其实是业务概念; 16 | * 后续将展示节点的出度、入度,废弃此字段 17 | */ 18 | value?: number 19 | } 20 | -------------------------------------------------------------------------------- /examples/hbx+vue3/uni.promisify.adaptor.js: -------------------------------------------------------------------------------- 1 | uni.addInterceptor({ 2 | returnValue (res) { 3 | if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { 4 | return res; 5 | } 6 | return new Promise((resolve, reject) => { 7 | res.then((res) => { 8 | if (!res) return resolve(res) 9 | return res[0] ? reject(res[0]) : resolve(res[1]) 10 | }); 11 | }); 12 | }, 13 | }); -------------------------------------------------------------------------------- /examples/hbx+vue3/main.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | 3 | // #ifndef VUE3 4 | import Vue from 'vue' 5 | import './uni.promisify.adaptor' 6 | Vue.config.productionTip = false 7 | App.mpType = 'app' 8 | const app = new Vue({ 9 | ...App 10 | }) 11 | app.$mount() 12 | // #endif 13 | 14 | // #ifdef VUE3 15 | import { createSSRApp } from 'vue' 16 | export function createApp() { 17 | const app = createSSRApp(App) 18 | return { 19 | app 20 | } 21 | } 22 | // #endif -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit", 5 | "source.organizeImports": "never" 6 | }, 7 | 8 | "editor.quickSuggestions": { 9 | "strings": "on" 10 | }, 11 | "editor.formatOnSave": false, 12 | "eslint.useFlatConfig": true, 13 | 14 | "yaml.schemas": { 15 | ".vscode/schema/commitlint-with-cz.json": ".commitlintrc.yaml" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/nunjucks/index.ts: -------------------------------------------------------------------------------- 1 | import nunjucks from 'nunjucks' 2 | 3 | /** 4 | * Setup Nunjucks environment 5 | */ 6 | export function setupNunjucks() { 7 | nunjucks.configure('') 8 | .addGlobal('now', () => { 9 | return new Date() 10 | }) 11 | .addFilter('date', (date, format) => { 12 | if (format === 'toLocaleString') { 13 | return date.toLocaleString() 14 | } 15 | return date.toLocaleString() 16 | }) 17 | return nunjucks 18 | } 19 | -------------------------------------------------------------------------------- /examples/biz-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "useDefineForClassFields": true, 7 | "baseUrl": "./", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "@/*": ["*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": ["@dcloudio/types"], 15 | "strict": true, 16 | "sourceMap": true, 17 | "esModuleInterop": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "ESNext"], 5 | "baseUrl": ".", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "skipDefaultLibCheck": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts"], 17 | "exclude": ["node_modules", "dist", "**/*.js"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/hbx+vue3/types/async-component.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by @uni-ku/bundle-optimizer 5 | declare module '*?async' { 6 | const component: any 7 | export = component 8 | } 9 | declare module '../../pages-sub/index/index.vue?async' { 10 | const component: typeof import('../../pages-sub/index/index.vue') 11 | export = component 12 | } 13 | declare module '@/pages-sub/index/index.vue?async' { 14 | const component: typeof import('@/pages-sub/index/index.vue') 15 | export = component 16 | } 17 | -------------------------------------------------------------------------------- /examples/hbx+vue3/types/async-import.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by @uni-ku/bundle-optimizer 5 | export {} 6 | 7 | interface ModuleMap { 8 | '../../pages-sub/index/index.vue': typeof import('../../pages-sub/index/index.vue') 9 | '../../pages-sub/plugins/index': typeof import('../../pages-sub/plugins/index') 10 | '@/pages-sub/plugins/index': typeof import('@/pages-sub/plugins/index') 11 | [path: string]: any 12 | } 13 | 14 | declare global { 15 | function AsyncImport(arg: T): Promise 16 | } 17 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | 4 | catalog: 5 | lodash: ^4.17.21 6 | '@types/lodash': ^4.17.13 7 | 8 | catalogs: 9 | uniapp: 10 | '@dcloudio/types': 3.4.14 11 | '@dcloudio/uni-app': 3.0.0-4060620250520001 12 | '@dcloudio/uni-components': 3.0.0-4060620250520001 13 | '@dcloudio/uni-h5': 3.0.0-4060620250520001 14 | '@dcloudio/uni-mp-weixin': 3.0.0-4060620250520001 15 | '@dcloudio/uni-automator': 3.0.0-4060620250520001 16 | '@dcloudio/uni-cli-shared': 3.0.0-4060620250520001 17 | '@dcloudio/vite-plugin-uni': 3.0.0-4060620250520001 18 | vue: 3.4.21 19 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages-sub/index/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "useDefineForClassFields": true, 7 | "baseUrl": "./", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "@/*": ["src/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": ["@dcloudio/types"], 15 | "strict": true, 16 | "sourceMap": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": [ 20 | "src/types", 21 | "src/**/*.ts", 22 | "src/**/*.d.ts", 23 | "src/**/*.tsx", 24 | "src/**/*.vue" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/base64url/pad-string.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | 3 | export default function padString(input: string): string { 4 | const segmentLength = 4 5 | const stringLength = input.length 6 | const diff = stringLength % segmentLength 7 | 8 | if (!diff) { 9 | return input 10 | } 11 | 12 | let position = stringLength 13 | let padLength = segmentLength - diff 14 | const paddedStringLength = stringLength + padLength 15 | const buffer = Buffer.alloc(paddedStringLength) 16 | 17 | buffer.write(input) 18 | 19 | while (padLength--) { 20 | buffer.write('=', position++) 21 | } 22 | 23 | return buffer.toString() 24 | } 25 | -------------------------------------------------------------------------------- /examples/biz-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "biz-components", 3 | "version": "1.0.0", 4 | "description": "components example", 5 | "author": "", 6 | "license": "ISC", 7 | "keywords": [], 8 | "main": "index.ts", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "peerDependencies": { 13 | "@dcloudio/uni-app": "catalog:uniapp", 14 | "@dcloudio/uni-components": "catalog:uniapp", 15 | "@dcloudio/uni-h5": "catalog:uniapp", 16 | "@dcloudio/uni-mp-weixin": "catalog:uniapp", 17 | "vue": "catalog:uniapp" 18 | }, 19 | "devDependencies": { 20 | "@dcloudio/types": "catalog:uniapp" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/query-string/types.ts: -------------------------------------------------------------------------------- 1 | export type SetRequired = Omit & Required> 2 | 3 | export type ArrayFormat = 'index' | 'bracket' | 'colon-list-separator' | 'comma' | 'separator' | 'bracket-separator' 4 | 5 | export interface Options { 6 | decode?: boolean 7 | encode?: boolean 8 | strict?: boolean 9 | arrayFormat?: ArrayFormat | 'none' 10 | arrayFormatSeparator?: string 11 | types?: Record 12 | } 13 | 14 | export type ParsedQuery = Record> 15 | 16 | export type ParserForArrayFormat = (key: string, value: string | undefined | null, accumulator: T) => void 17 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages 3 | { 4 | "path": "pages/index/index", 5 | "style": { 6 | "navigationBarTitleText": "uni-app" 7 | } 8 | } 9 | ], 10 | "subPackages": [ 11 | { 12 | "root": "pages-sub", 13 | "pages": [ 14 | { 15 | "path" : "index/index", 16 | "style" : 17 | { 18 | "navigationBarTitleText" : "" 19 | } 20 | } 21 | ] 22 | } 23 | ], 24 | "globalStyle": { 25 | "navigationBarTextStyle": "black", 26 | "navigationBarTitleText": "uni-app", 27 | "navigationBarBackgroundColor": "#F8F8F8", 28 | "backgroundColor": "#F8F8F8" 29 | }, 30 | "uniIdRouter": {} 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/schema/readme.md: -------------------------------------------------------------------------------- 1 | ## commitlint - schema 2 | 3 | - [commitlint 主配置](./commitlint.json) - 更新地址 [conventional-changelog/commitlint](https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/config-validator/src/commitlint.schema.json) 4 | - [cz-git 配置](./cz-git.json) - 更新地址 [cz-git](https://github.com/Zhengqbbb/cz-git/blob/main/docs/public/schema/cz-git.json) 5 | 6 | > [commitlint-patch.json](./commitlint-patch.json) 是在 [commitlint.json](./commitlint.json) 基础上的修改。 7 | > 8 | >因为 [`"oneOf"`](./commitlint.json#L6) 配置有点问题,详见 https://github.com/redhat-developer/vscode-yaml/issues/247 ,故去除。 9 | > 10 | > [commitlint-with-cz.json](./commitlint-with-cz.json) 是调整合并之后的完整配置,在 [settings.json - yaml.schemas](../settings.json) 中有映射配置。 11 | -------------------------------------------------------------------------------- /examples/hbx+vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/utils/lex-parse/type.d.ts: -------------------------------------------------------------------------------- 1 | /** 解析函数的参数,考虑字符串、数字、变量等类型,并返回定位信息 */ 2 | export interface ArgumentLocation { 3 | value: string | number 4 | start: number 5 | end: number 6 | } 7 | 8 | export interface FullMatchLocation { 9 | start: number 10 | end: number 11 | fullMatch: string 12 | } 13 | 14 | export interface FunctionCall { 15 | full: FullMatchLocation 16 | args: ArgumentLocation[] 17 | } 18 | 19 | /** 解析 `import xxx from 'yyy?query1&query2'` 的导入形式,返回定位信息 */ 20 | export interface ImportDefaultWithQuery { 21 | /** import xxx from 'yyy?query1&query2' */ 22 | full: FullMatchLocation 23 | /** xxx */ 24 | defaultVariable: ArgumentLocation 25 | /** yyy */ 26 | modulePath: ArgumentLocation 27 | /** ?query1&query2 */ 28 | query: ArgumentLocation[] 29 | /** 完整路径信息 */ 30 | fullPath: ArgumentLocation 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/visualizer/type/base.restrict.ts: -------------------------------------------------------------------------------- 1 | import type { GraphBaseNode } from './base.node' 2 | 3 | /** 4 | * 图形受限域 5 | */ 6 | export interface GraphRestrictArea { 7 | /** 8 | * 唯一标识 9 | * @description 如果是文件系统可以用相同系列模块的基础路径,当然不作限制 10 | */ 11 | id: string 12 | /** 简短文本 */ 13 | name: string 14 | /** 业务需求的显示文本 */ 15 | label?: string 16 | /** 17 | * 限制等级 18 | * @description 起始等级为0,限制等级越高,说明深入的层次越深,具体业务含义可自行赋予 19 | */ 20 | level?: number 21 | } 22 | 23 | /** 受限锚点 */ 24 | export interface GraphRestrictAnchor { 25 | /** 受限域信息 */ 26 | area: GraphRestrictArea 27 | /** 28 | * 唯一标识 29 | * @description 如果是文件系统可以用文件路径,不作限制 30 | */ 31 | id: string 32 | } 33 | 34 | /** 受限节点 */ 35 | export interface RestrictGraphNode extends GraphRestrictAnchor, GraphBaseNode { 36 | } 37 | -------------------------------------------------------------------------------- /examples/hbx+vue3/vite.config.js: -------------------------------------------------------------------------------- 1 | import Uni from '@dcloudio/vite-plugin-uni' 2 | import Optimization from '@uni-ku/bundle-optimizer' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | Uni(), 8 | // 可以无需传递任何参数,默认开启所有插件功能,并在项目根目录生成类型定义文件 9 | Optimization({ 10 | // 插件功能开关,默认为true,即开启所有功能 11 | enable: { 12 | 'optimization': true, 13 | 'async-import': true, 14 | 'async-component': true, 15 | }, 16 | // dts文件输出配置,默认为true,即在项目根目录生成类型定义文件 17 | dts: { 18 | 'enable': true, 19 | 'base': './types', 20 | }, 21 | // 也可以传递具体的子插件的字符串列表,如 ['optimization', 'async-import', 'async-component'],开启部分插件的log功能 22 | logger: true, 23 | }), 24 | ], 25 | resolve: { 26 | alias: { 27 | '#/*': '/src', 28 | } 29 | } 30 | }) -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | tags: 9 | - '!**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: 10 25 | 26 | - name: Set node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version-file: .nvmrc 30 | cache: pnpm 31 | registry-url: https://registry.npmjs.org 32 | 33 | - name: Install 34 | run: pnpm install 35 | 36 | - name: Build 37 | run: pnpm build 38 | 39 | - name: Release 40 | run: pnpm dlx pkg-pr-new publish --compact --pnpm --only-templates 41 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "path": "pages/index", 5 | "style": { 6 | "navigationBarTitleText": "uni-app" 7 | } 8 | } 9 | ], 10 | "globalStyle": { 11 | "navigationBarTextStyle": "black", 12 | "navigationBarTitleText": "uni-app", 13 | "navigationBarBackgroundColor": "#F8F8F8", 14 | "backgroundColor": "#F8F8F8" 15 | }, 16 | "subPackages": [ 17 | { 18 | "root": "pages-sub-demo", 19 | "pages": [ 20 | { 21 | "path": "index", 22 | "style": { 23 | "navigationBarTitleText": "子包示例" 24 | } 25 | } 26 | ] 27 | }, 28 | { 29 | "root": "pages-sub-async/", 30 | "pages": [ 31 | { 32 | "path": "index", 33 | "style": { 34 | "navigationBarTitleText": "pages-sub-async" 35 | } 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/query-string/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from './types' 2 | import decodeComponent from '../decode-uri-component' 3 | 4 | export function strictUriEncode(str: string) { 5 | return encodeURIComponent(str).replaceAll(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`) 6 | } 7 | 8 | export function isNil(value: unknown): value is null | undefined { 9 | return value === null || value === undefined 10 | } 11 | 12 | export function isSingleChar(char: string): char is string & { length: 1 } { 13 | return char.length === 1 14 | } 15 | 16 | export function encode(value: string, options: Options = {}) { 17 | if (options.encode) { 18 | return options.strict ? strictUriEncode(value) : encodeURIComponent(value) 19 | } 20 | 21 | return value 22 | } 23 | 24 | export function decode(value: string, options: Options = {}) { 25 | if (options.decode) { 26 | return decodeComponent(value) 27 | } 28 | 29 | return value 30 | } 31 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | Hey There 💜, 感谢参与贡献!在提交您的贡献之前,请务必花点时间阅读以下指南: 4 | 5 | - [行为准则](./CODE_OF_CONDUCT.md) 6 | 7 | ## 参与开发 8 | 9 | ### 克隆 10 | 11 | ``` 12 | git clone https://github.com/uni-ku/bundle-optimizer.git 13 | ``` 14 | 15 | ### 起手 16 | 17 | - 我们需要使用 `pnpm` 作为包管理器 18 | - 安装依赖 `pnpm install` 19 | - 打包项目 `pnpm build` 20 | 21 | ### 代码 22 | 23 | - 我们使用 `ESLint` 来检查和格式化代码 24 | - 请确保代码可以通过仓库 `ESLINT` 验证 25 | 26 | ### 测试 27 | 28 | - 如果是新增新功能,我们希望有测试代码 29 | - 运行测试 `pnpm test` 30 | 31 | ### Commit 32 | 33 | 我们使用 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范,若不满足将会被拦截 34 | 35 | > `git add` 后可通过 `git cz` 提交Commit,对不熟悉的朋友会更加便利且友好 36 | 37 | ### Pull Request 38 | 39 | #### 参考 40 | 41 | 如果你的第一次参与贡献,可以先通过以下文章快速入门: 42 | 43 | - [第一次参与开源](https://github.com/firstcontributions/first-contributions/blob/main/translations/README.zh-cn.md) 44 | 45 | #### 规范 46 | 47 | 尽量避免多个不同功能的 `Commit` 放置在一个 `PR` 中,若出现这种情况,那么我们将会让其压缩成一个 `Commit` 合并 48 | -------------------------------------------------------------------------------- /src/utils/regexp/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转义正则特殊字符 3 | */ 4 | export function escapeRegExp(str: string) { 5 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 6 | } 7 | 8 | /** 9 | * 创建匹配前缀的正则表达式 10 | * @param {string} prefix - 要匹配的前缀字符串 11 | * @param {string} flags - 正则标志,默认为 ''(不设置全局匹配,通常用于单个前缀匹配) 12 | * @returns {RegExp} 构建好的正则表达式 13 | */ 14 | export function createPrefixRegex(prefix: string, flags: string = ''): RegExp { 15 | // 对特殊字符进行转义 16 | const escapedPrefix = escapeRegExp(prefix) 17 | // 使用 '^' 锚定开头,确保匹配的是前缀 18 | return new RegExp(`^${escapedPrefix}`, flags) 19 | } 20 | 21 | /** 22 | * 创建匹配开头或结尾字符的正则表达式 23 | * @param {string} char - 要匹配的特定字符(可选,默认匹配任意字符) 24 | * @param {string} flags - 正则标志,默认为 'g' 25 | * @returns {RegExp} 构建好的正则表达式 26 | */ 27 | export function createEdgeRegex(char: string = '.', flags: string = 'g'): RegExp { 28 | // 对特殊字符进行转义 29 | const escapedChar = escapeRegExp(char) 30 | return new RegExp(`^${escapedChar}|${escapedChar}$`, flags) 31 | } 32 | -------------------------------------------------------------------------------- /src/common/AsyncImports.ts: -------------------------------------------------------------------------------- 1 | export class AsyncImports { 2 | cache: Map = new Map() 3 | valueMap: Map = new Map() 4 | 5 | addCache(id: string, value: string, realPath?: string) { 6 | if (this.cache.has(id) && !this.cache.get(id)?.includes(value)) { 7 | this.cache.get(id)?.push(value) 8 | } 9 | else if (!this.cache.has(id)) { 10 | this.cache.set(id, [value]) 11 | } 12 | 13 | if (!realPath) { 14 | return 15 | } 16 | 17 | if (this.valueMap.has(value) && !this.valueMap.get(value)?.includes(realPath)) { 18 | this.valueMap.get(value)?.push(realPath) 19 | } 20 | else { 21 | this.valueMap.set(value, [realPath]) 22 | } 23 | } 24 | 25 | getCache(id: string) { 26 | return this.cache.get(id) 27 | } 28 | 29 | getRealPath(value: string) { 30 | return this.valueMap.get(value) 31 | } 32 | 33 | clearCache() { 34 | this.cache.clear() 35 | this.valueMap.clear() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | /** 4 | * `js`|`jsx`|`ts`|`uts`|`tsx`|`mjs`|`json` 5 | * @description json 文件会被处理成 js 模块 6 | */ 7 | export const EXTNAME_JS_RE = /\.(js|jsx|ts|uts|tsx|mjs|json)$/ 8 | export const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/ 9 | 10 | export const knownJsSrcRE 11 | = /\.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba|mdx)(?:$|\?)/ 12 | 13 | export const CSS_LANGS_RE 14 | = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ 15 | 16 | /** `assets` 或者 `./assets` 开头的文件夹 */ 17 | export const ASSETS_DIR_RE = /^(\.?\/)?assets\// 18 | 19 | /** `src` 或者 `./src` 开头的文件夹 */ 20 | export const SRC_DIR_RE = /^(\.?\/)?src\// 21 | 22 | /** 文件后缀 */ 23 | export const EXT_RE = /\.\w+$/ 24 | 25 | export function isCSSRequest(request: string): boolean { 26 | return CSS_LANGS_RE.test(request) 27 | } 28 | 29 | /** 30 | * 项目根路径 31 | * 32 | * // TODO: 后续自实现项目根路径的查找 33 | */ 34 | export const ROOT_DIR = process.env.VITE_ROOT_DIR! 35 | if (!ROOT_DIR) { 36 | throw new Error('`ROOT_DIR` is not defined') 37 | } 38 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 34 | 35 | 48 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | workflow_dispatch: # 允许手动触发 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | run_install: false 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 # Node.js 版本 30 | cache: pnpm # 缓存 pnpm 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Build Library 36 | run: pnpm run build 37 | 38 | - name: Build Project 39 | run: pnpm run example1:build:h5 40 | 41 | - name: Deploy to GitHub Pages 42 | uses: peaceiris/actions-gh-pages@v4 43 | with: 44 | github_token: ${{ secrets.GITHUB_TOKEN }} 45 | publish_dir: ./examples/vue3+vite+ts/dist/build/h5 # 构建输出目录 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present, Vanisper 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/vue3+vite+ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "uni", 7 | "build": "uni build", 8 | "dev:mp-weixin": "uni -p mp-weixin", 9 | "build:mp-weixin": "uni build -p mp-weixin" 10 | }, 11 | "dependencies": { 12 | "@dcloudio/uni-app": "catalog:uniapp", 13 | "@dcloudio/uni-components": "catalog:uniapp", 14 | "@dcloudio/uni-h5": "catalog:uniapp", 15 | "@dcloudio/uni-mp-weixin": "catalog:uniapp", 16 | "@https-enable/colors": "^0.1.1", 17 | "@lixin-sdk/common-error-http-sdk": "workspace:*", 18 | "biz-components": "workspace:*", 19 | "lodash": "catalog:", 20 | "vue": "catalog:uniapp" 21 | }, 22 | "devDependencies": { 23 | "@dcloudio/types": "catalog:uniapp", 24 | "@dcloudio/uni-automator": "catalog:uniapp", 25 | "@dcloudio/uni-cli-shared": "catalog:uniapp", 26 | "@dcloudio/vite-plugin-uni": "catalog:uniapp", 27 | "@https-enable/types": "^0.1.1", 28 | "@types/lodash": "catalog:", 29 | "@uni-helper/vite-plugin-uni-components": "^0.2.0", 30 | "@uni-ku/bundle-optimizer": "workspace:*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 贡献者公约行为准则 2 | 3 | ## 承诺 4 | 5 | 为了促进一个开放和包容的环境,作为贡献者和维护者,我们承诺为每个人提供无骚扰的参与项目和社区的体验,无论年龄、身体大小、残疾、种族、性别特征、性别认同和表达、经验水平、教育、社会经济地位、国籍、个人外貌、种族、宗教或性别认同和取向等。 6 | 7 | ## 行为标准 8 | 9 | 促进积极环境的行为准则: 10 | 11 | - 使用欢迎和包容性的语言 12 | - 尊重不同的观点和经验 13 | - 欢迎建设性批评 14 | - 关注社区最新最好的技术,行为准则等 15 | - 对其他社区成员展示友好 16 | 17 | 不可接受行为示例: 18 | 19 | - 性化语言或图像等 20 | - 挑衅、侮辱、贬低的评论和个人或政治攻击 21 | - 骚扰 22 | - 未经明确允许,发布他人的私人信息 23 | - 其他在职业环境中可以被视为不合适的行为 24 | 25 | ## 责任感 26 | 27 | 项目维护者负责明确可接受行为的标准,并应对任何不可接受行为采取适当和公正的纠正措施。 28 | 29 | 项目维护者有权和责任删除、编辑或拒绝评论、提交、代码、wiki、issue 和其他不符合本行为准则的贡献,暂时或永久禁止任何贡献者参与其他不适当、具有威胁性、冒犯性或有害的行为。 30 | 31 | ## 范围 32 | 33 | 本行为准则适用于所有项目,并且当个人在公共空间代表项目或其社区时也适用。代表项目或社区的示例包括使用官方项目电子邮件地址,通过官方社交媒体账户发布内容,或在在线或离线活动中担任指定代表。项目的代表可以由项目维护者进一步定义和澄清。 34 | 35 | ## 执行 36 | 37 | 如有骚扰或其他不可接受的行为,可以通过联系项目团队 [📪](mailto:273266469@qq.com) 来报告。所有投诉将被审理和调查,在必要和适当的情况下会给予答复。项目团队将会对事件的报告者保密。特定的进一步详细信息可能会单独发布。 38 | 39 | 不遵守或不诚信执行行为准则的项目维护人员可能会面临由项目管理者或其他成员决定的暂时或永久的封禁。 40 | 41 | ## 版权声明 42 | 43 | 本行为准则改编自贡献者公约,版本1.4,可在 [code-of-conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 获得。 44 | 45 | 有关此行为准则的常见问题的答案,请参见 [Q&A](https://www.contributor-covenant.org/faq)。 46 | -------------------------------------------------------------------------------- /src/utils/segment-iterator/type.ts: -------------------------------------------------------------------------------- 1 | /** 段落位置 */ 2 | export interface SegmentPosition { 3 | /** 位置类型 */ 4 | type: 'full' | 'separator' | 'hard' 5 | /** 位置索引 */ 6 | index: number 7 | /** 断点字符 */ 8 | char?: string 9 | } 10 | 11 | /** 段落信息 */ 12 | export interface SegmentInfo { 13 | segment: string 14 | index: number 15 | total: number 16 | isFirst: boolean 17 | isLast: boolean 18 | position: SegmentPosition 19 | } 20 | 21 | /** 段落迭代器配置 */ 22 | export interface SegmentIteratorOptions { 23 | /** 24 | * 最大段落长度 25 | * @description 超过该长度则进行分段 26 | */ 27 | maxLength?: number 28 | /** 段落断点字符列表 */ 29 | breakChars?: string[] 30 | /** 31 | * 搜索断点字符的范围 32 | * @description 从 maxLength 向前和向后各扩展该范围进行断点搜索 33 | */ 34 | searchRange?: number 35 | /** 计数时是否包含分隔符 */ 36 | includeSeparator?: boolean 37 | } 38 | 39 | /** 段落迭代器 */ 40 | export interface SegmentIterator extends Iterable { 41 | toArray: () => SegmentInfo[] 42 | map: (callback: (info: SegmentInfo, index: number, total: number) => T) => T[] 43 | join: (separator?: string, callback?: (info: SegmentInfo) => string) => string 44 | } 45 | 46 | export type SegmentCallback = (info: SegmentInfo, index: number, total: number) => string 47 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import { TextEncoder } from 'node:util' 3 | import { xxhashBase16, xxhashBase36, xxhashBase64Url } from './xxhash' 4 | 5 | let textEncoder: TextEncoder 6 | 7 | // 导出哈希函数 8 | export const getHash64 = (input: string | Uint8Array) => xxhashBase64Url(ensureBuffer(input)) 9 | export const getHash36 = (input: string | Uint8Array) => xxhashBase36(ensureBuffer(input)) 10 | export const getHash16 = (input: string | Uint8Array) => xxhashBase16(ensureBuffer(input)) 11 | 12 | export const hasherByType = { 13 | base36: getHash36, 14 | base64: getHash64, 15 | hex: getHash16, 16 | } 17 | 18 | export function ensureBuffer(input: string | Uint8Array): Uint8Array { 19 | if (typeof input === 'string') { 20 | if (typeof Buffer === 'undefined') { 21 | textEncoder ??= new TextEncoder() 22 | return textEncoder.encode(input) 23 | } 24 | 25 | return Buffer.from(input) 26 | } 27 | return input 28 | } 29 | 30 | // // 测试代码 31 | // const input = 'const aaa = {\n name: "aaa"\n};\nexport {\n aaa as default\n};\n' 32 | // console.log('Base64:', getHash64(input)) // 'y7k522bi1wlPA3twufAYe' 33 | // // console.log('Base36:', getHash36(input)) 34 | // // console.log('Base16:', getHash16(input)) 35 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-demo/components/demo1.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 50 | -------------------------------------------------------------------------------- /examples/common-error-http-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lixin-sdk/common-error-http-sdk", 3 | "version": "0.6.9", 4 | "description": "grpc-http 包", 5 | "author": "lixin tech", 6 | "license": "ISC", 7 | "keywords": [], 8 | "exports": { 9 | "./lixin/common/error/v1/rpc": "./lixin/common/error/v1/rpc.ts", 10 | "./lixin/common/error/v1/error": "./lixin/common/error/v1/error.ts", 11 | "./lixin/common/error/v1/error.enum": "./lixin/common/error/v1/error.enum.ts", 12 | "./v1/rpc": "./lixin/common/error/v1/rpc.ts", 13 | "./v1/error": "./lixin/common/error/v1/error.ts", 14 | "./v1/error.enum": "./lixin/common/error/v1/error.enum.ts" 15 | }, 16 | "main": "index.js", 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "typesVersions": { 21 | "*": { 22 | "./lixin/common/error/v1/rpc": [ 23 | "lixin/common/error/v1/rpc.ts" 24 | ], 25 | "./lixin/common/error/v1/error": [ 26 | "lixin/common/error/v1/error.ts" 27 | ], 28 | "./lixin/common/error/v1/error.enum": [ 29 | "lixin/common/error/v1/error.enum.ts" 30 | ], 31 | "./v1/rpc": [ 32 | "lixin/common/error/v1/rpc.ts" 33 | ], 34 | "./v1/error": [ 35 | "lixin/common/error/v1/error.ts" 36 | ], 37 | "./v1/error.enum": [ 38 | "lixin/common/error/v1/error.enum.ts" 39 | ] 40 | } 41 | }, 42 | "dependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | 70 | -------------------------------------------------------------------------------- /src/utils/visualizer/type/vite.ts: -------------------------------------------------------------------------------- 1 | import type { GraphLink } from './base.link' 2 | import type { GraphBaseNode } from './base.node' 3 | import type { RestrictGraphNode } from './base.restrict' 4 | 5 | enum ViteNodeType { 6 | ASSET = 'asset', 7 | CHUNK = 'chunk', 8 | } 9 | 10 | interface BaseNode { 11 | /** 12 | * 节点类别 13 | * @description 分组作用 14 | */ 15 | category?: string 16 | 17 | /** 18 | * 节点类别索引 19 | * @description 此处主要是为了兼容 echarts,取分类在列表的索引 20 | */ 21 | categoryIndex?: number 22 | } 23 | 24 | export type ViteBaseNode = BaseNode & GraphBaseNode<`${ViteNodeType}`> 25 | export type ViteRestrictBaseNode = BaseNode & RestrictGraphNode<`${ViteNodeType}`> 26 | 27 | interface ChunkNode { 28 | type: 'chunk' 29 | /** 是否为打包入口 */ 30 | isEntry: boolean 31 | code?: string | null 32 | } 33 | 34 | interface AssetNode { 35 | type: 'asset' 36 | /** 37 | * 资源类型 38 | * @description 一般是文件扩展名 39 | */ 40 | resourceType: string 41 | /** 资源内容 */ 42 | source?: string | Uint8Array 43 | } 44 | 45 | export type ViteChunkNode = ChunkNode & ViteBaseNode 46 | export type ViteAssetNode = AssetNode & ViteBaseNode 47 | export type ViteNode = ViteChunkNode | ViteAssetNode 48 | 49 | export type ViteRestrictChunkNode = ChunkNode & ViteRestrictBaseNode 50 | export type ViteRestrictAssetNode = AssetNode & ViteRestrictBaseNode 51 | export type ViteRestrictNode = ViteRestrictChunkNode | ViteRestrictAssetNode 52 | 53 | enum ViteNodeLinkType { 54 | STATIC = 'static', 55 | DYNAMIC = 'dynamic', 56 | } 57 | 58 | export type ViteNodeLink = GraphLink<`${ViteNodeLinkType}`> 59 | -------------------------------------------------------------------------------- /src/utils/split-on-first/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Splits a string into two parts based on the first occurrence of a separator. 3 | * @param str The input string to split. 4 | * @param separator The separator to use for splitting the string. 5 | * @returns An array containing the two parts of the split string. 6 | * 7 | * @link https://github.com/sindresorhus/split-on-first 8 | */ 9 | export function splitOnFirst(str: string, separator: string | RegExp): [] | [null, null] | [string, string] { 10 | if (!(typeof str === 'string' && (typeof separator === 'string' || separator instanceof RegExp))) { 11 | throw new TypeError('Expected the arguments to be of type `string` and `RegExp`') 12 | } 13 | 14 | if (str === '' || separator === '') { 15 | return [] 16 | } 17 | 18 | let separatorIndex = -1 19 | let matchLength = 1 20 | 21 | if (typeof separator === 'string') { 22 | separatorIndex = str.indexOf(separator) 23 | matchLength = separator.length 24 | } 25 | // https://github.com/sindresorhus/split-on-first/issues/12 26 | else if (separator instanceof RegExp) { 27 | // ignore `g` flag 28 | const nonGlobalRegex = new RegExp(separator.source, separator.flags.replace('g', '')) 29 | const match = nonGlobalRegex.exec(str) 30 | if (match) { 31 | separatorIndex = match.index 32 | matchLength = match[0].length 33 | } 34 | } 35 | 36 | if (separatorIndex === -1) { 37 | return [] 38 | } 39 | 40 | return [ 41 | str.slice(0, separatorIndex), 42 | str.slice(separatorIndex + matchLength), 43 | ] 44 | } 45 | 46 | export default splitOnFirst 47 | -------------------------------------------------------------------------------- /src/utils/crypto/xxhash.ts: -------------------------------------------------------------------------------- 1 | // `rollup/rust/xxhash/src/lib.rs` 的 `node` 实现 2 | // 3 | // 4 | 5 | import { Buffer } from 'node:buffer' 6 | import { xxh3 } from '@node-rs/xxhash' 7 | import { toString } from './base_encode' 8 | 9 | // 将 `BigInt` 转换为小端字节序的 `Uint8Array` | 对标 rust 中的 `u128.to_le_bytes()` 10 | function toLeBytes(value: bigint, byteLength: number): Uint8Array { 11 | const buffer = Buffer.alloc(byteLength) 12 | buffer.writeBigUInt64LE(value & BigInt('0xFFFFFFFFFFFFFFFF'), 0) 13 | buffer.writeBigUInt64LE(value >> BigInt(64), 8) 14 | return new Uint8Array(buffer) 15 | } 16 | 17 | // 定义字符集 18 | const CHARACTERS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' 19 | const CHARACTERS_BASE36 = 'abcdefghijklmnopqrstuvwxyz0123456789' 20 | const CHARACTERS_BASE16 = '0123456789abcdef' 21 | 22 | // 计算哈希值并编码为 Base64 URL 格式 23 | export function xxhashBase64Url(input: Uint8Array) { 24 | const hashBigInt = xxh3.xxh128(input) 25 | const hashBuffer = toLeBytes(hashBigInt, 16) 26 | 27 | return toString(new Uint8Array(hashBuffer), 64, CHARACTERS_BASE64) 28 | } 29 | 30 | // 计算哈希值并编码为 Base36 格式 31 | export function xxhashBase36(input: Uint8Array) { 32 | const hashBigInt = xxh3.xxh128(input) 33 | const hashBuffer = toLeBytes(hashBigInt, 16) 34 | return toString(new Uint8Array(hashBuffer), 36, CHARACTERS_BASE36) 35 | } 36 | 37 | // 计算哈希值并编码为 Base16 格式 38 | export function xxhashBase16(input: Uint8Array) { 39 | const hashBigInt = xxh3.xxh128(input) 40 | const hashBuffer = toLeBytes(hashBigInt, 16) 41 | return toString(new Uint8Array(hashBuffer), 16, CHARACTERS_BASE16) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/visualizer/README.md: -------------------------------------------------------------------------------- 1 | # visualizer 2 | 3 | 依赖关系可视化基础工具 4 | 5 | --- 6 | 7 | ## 基础概念 8 | 9 | ### 节点与链接(Node & Link) 10 | 11 | > Node & Link 是整个体系的基础概念 12 | 13 | 1. **Node** 14 | * 体系下的真实个体 15 | * 有权重的(以节点之间关系的入度(in-degree)为依据,作为该节点的权重) 16 | * 有类型的(业务中可自行对其赋予具体意义) 17 | 18 | 2. **Link** 19 | * 个体之间的关系描述 20 | * 有指向性的 21 | * 有类型的(业务中可自行对其赋予具体意义) 22 | 23 | ### 受限域与锚点(Area & Anchor) 24 | 25 | > 26 | > “域”和“锚点”都对节点有**约束**作用,所以在这两个的概念之前加上了“**受限**” (`restrict`) 字样。 27 | > 28 | > Area 起到了“归集”、“管理”的作用;就像后台管理系统商品模块中的“商品管理”,节点可以类比成被管理的“商品”; 29 | > 30 | > 就是这个“管理”的行为,“被管理”的节点就“受限”了,变成了受限节点。 31 | > 32 | > 至于“锚点”的“**锚**”,其实是为了区别于普通节点, 33 | > 因为这类受限节点是直接被“域”管理的,其他的普通节点没有这个限制; 34 | > 在可视化时,这些普通节点其实是“游离”的(在没有受限之前,所有节点都是地位平等的、游离的), 35 | > 而受限节点受到域的管理,集中到了一起。 36 | > 37 | > 在单独的某一个域的内部视角中,由于节点之间存在关系链接 (`Link`), 38 | > 那些游离节点就像是受到了受限节点的“牵引”,被拉进了受限域内,所以称“受限节点”为“锚点”。 39 | > 40 | 41 | 1. **Area** 42 | * 有等级的,起始等级为0,限制等级越高,说明深入的层次越深,具体业务含义可自行赋予 43 | * 受限等级可以形成整个体系的组织结构或者分析的深度,可以理解成是一个多级分类的指标 44 | 45 | > 46 | > 受限等级业务示例场景——小程序包体系: 47 | > * 小程序包体系中的主包可以记为等级0,各子包记为等级1; 48 | > * 吸引:锚点指向的游离节点,这一节点之间的关系即为锚点向该游离节点的“吸引”行为;而锚点受“域”管理,所以也是域对该游离节点的吸引行为。 49 | > * 吸引原则:受限域可以“吸引”不高于当前等级的“游离节点公共域”;如果某一个“游离节点公共域”只被一个受限域吸引(其实也能证明该公共域等级与该受限域等级一致),那么这个公共域可以并入受限域;“锚点”不能被其他域吸引,即使等级相同,他是当前受限域的组成部分,只属于当前受限域。 50 | > * 游离节点公共域:在吸引力没有作用之前,游离节点共同组成了一个巨大的公共域,等级为0; 51 | > * 游离节点公共域分裂原则:吸引力作用之后,“游离节点公共域”将会分裂,形成等级区分,同时不能破坏原来各“引力域”对游离节点的吸引关系结构; 52 | > * 一个游离节点同时被不同等级的受限域吸引,该节点进入其中最小等级的公共域(类似于公共模块的复用原则),这样就不会破坏原来的吸引关系 53 | > * 游离节点公共域的被迫降级:如果一个公共域由于不可抗力的原因,无法在某一等级内“驻足”,或者说无法存在实体支撑,那么该公共域会被迫降1级,汇入低1级的公共域内; 54 | > * 这个降级行为可以连续多级,直到稳定; 55 | > * 这种“不可抗力”,在物理文件系统的体系下,大概是因为这个公共域没有实体支撑,于是塌陷到了低一等级的公共域内了; 56 | > * 有别于原生存在的公共域,被迫降级的公共域,其实是没有被降级之后等级的受限域吸引的; 57 | > * 但是就是这样的一个降级行为,降级之后的等级的受限域就可以吸引这个原来无法吸引的公共域了(当然,这里是存在一个因果关系的,只是理论上产生了如此的可能而已); 58 | > 59 | 60 | 2. **Anchor** 61 | * 关联受限域的(area) 62 | 63 | --- 64 | -------------------------------------------------------------------------------- /src/utils/base64url/index.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import padString from './pad-string' 3 | 4 | function encode(input: string | Buffer, encoding: BufferEncoding = 'utf8'): string { 5 | if (Buffer.isBuffer(input)) { 6 | return fromBase64(input.toString('base64')) 7 | } 8 | return fromBase64(Buffer.from(input, encoding).toString('base64')) 9 | }; 10 | 11 | function decode(base64url: string, encoding: BufferEncoding = 'utf8'): string { 12 | return Buffer.from(toBase64(base64url), 'base64').toString(encoding) 13 | } 14 | 15 | function toBase64(base64url: string | Buffer): string { 16 | // We this to be a string so we can do .replace on it. If it's 17 | // already a string, this is a noop. 18 | base64url = base64url.toString() 19 | return padString(base64url) 20 | .replace(/-/g, '+') 21 | .replace(/_/g, '/') 22 | } 23 | 24 | function fromBase64(base64: string): string { 25 | return base64 26 | .replace(/=/g, '') 27 | .replace(/\+/g, '-') 28 | .replace(/\//g, '_') 29 | } 30 | 31 | function toBuffer(base64url: string): Buffer { 32 | return Buffer.from(toBase64(base64url), 'base64') 33 | } 34 | 35 | export interface Base64Url { 36 | (input: string | Buffer, encoding?: BufferEncoding): string 37 | encode: (input: string | Buffer, encoding?: BufferEncoding) => string 38 | decode: (base64url: string, encoding?: BufferEncoding) => string 39 | toBase64: (base64url: string | Buffer) => string 40 | fromBase64: (base64: string) => string 41 | toBuffer: (base64url: string) => Buffer 42 | } 43 | 44 | /** 45 | * @link https://github.com/brianloveswords/base64url 46 | */ 47 | const base64url = encode as Base64Url 48 | 49 | base64url.encode = encode 50 | base64url.decode = decode 51 | base64url.toBase64 = toBase64 52 | base64url.fromBase64 = fromBase64 53 | base64url.toBuffer = toBuffer 54 | 55 | export default base64url 56 | -------------------------------------------------------------------------------- /examples/hbx+vue3/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color:#333;//基本色 25 | $uni-text-color-inverse:#fff;//反色 26 | $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable:#c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color:#ffffff; 32 | $uni-bg-color-grey:#f8f8f8; 33 | $uni-bg-color-hover:#f1f1f1;//点击状态颜色 34 | $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color:#c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm:12px; 43 | $uni-font-size-base:14px; 44 | $uni-font-size-lg:16px; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm:20px; 48 | $uni-img-size-base:26px; 49 | $uni-img-size-lg:40px; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 2px; 53 | $uni-border-radius-base: 3px; 54 | $uni-border-radius-lg: 6px; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 5px; 59 | $uni-spacing-row-base: 10px; 60 | $uni-spacing-row-lg: 15px; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 4px; 64 | $uni-spacing-col-base: 8px; 65 | $uni-spacing-col-lg: 12px; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2C405A; // 文章标题颜色 72 | $uni-font-size-title:20px; 73 | $uni-color-subtitle: #555555; // 二级标题颜色 74 | $uni-font-size-subtitle:26px; 75 | $uni-color-paragraph: #3F536E; // 文章段落颜色 76 | $uni-font-size-paragraph:15px; 77 | -------------------------------------------------------------------------------- /src/utils/crypto/base_encode.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import { Buffer } from 'node:buffer' 4 | 5 | // 将字节数组编码为指定基数的字符串 6 | export function encode(buf: Uint8Array, base: number): number[] { 7 | let num = BigInt(`0x${Buffer.from(buf).toString('hex')}`) 8 | const digits: number[] = [] 9 | 10 | while (num > 0) { 11 | digits.push(Number(num % BigInt(base))) 12 | num = num / BigInt(base) 13 | } 14 | 15 | const zeros = buf.findIndex(byte => byte !== 0) 16 | if (zeros !== -1) { 17 | digits.push(...Array.from({ length: zeros }).fill(0)) 18 | } 19 | 20 | digits.reverse() 21 | return digits 22 | } 23 | 24 | // 将指定基数的编码字符串解码为字节数组 25 | export function decode(buf: number[], base: number): Uint8Array | null { 26 | let num = BigInt(0) 27 | const zeros = buf.findIndex(digit => digit !== 0) 28 | if (zeros !== -1) { 29 | num = BigInt(zeros) 30 | } 31 | 32 | for (const digit of buf) { 33 | if (digit >= base) { 34 | return null 35 | } 36 | num = num * BigInt(base) + BigInt(digit) 37 | } 38 | 39 | const hex = num.toString(16) 40 | const bytes = Buffer.from(hex.length % 2 ? `0${hex}` : hex, 'hex') 41 | return new Uint8Array(bytes) 42 | } 43 | 44 | // 将字节数组转换为指定字符表的字符串 45 | export function toString(buf: Uint8Array, base: number, chars: string): string | null { 46 | return encode(buf, base) 47 | .map(digit => chars[digit]) 48 | .join('') 49 | } 50 | 51 | // 将指定字符表的字符串转换为字节数组 52 | export function fromStr(string: string, base: number, chars: string): Uint8Array | null { 53 | const buf = Array.from(string).map(char => chars.indexOf(char)) 54 | return decode(buf, base) 55 | } 56 | 57 | // // 测试代码 58 | // const data = new Uint8Array([0x27, 0x10]) 59 | // console.log(encode(data, 10)) // [1, 0, 0, 0, 0] 60 | 61 | // console.log('Decoded:', fromStr('255', 10, '0123456789')) 62 | // console.log('Encoded:', toString(new Uint8Array([0xA]), 2, 'OX')) // XOXO 63 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-async/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 59 | 60 | 75 | -------------------------------------------------------------------------------- /src/common/AsyncComponents.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | export type BindingAsyncComponents = Record 8 | 9 | export interface TemplateDescriptor { 10 | bindingAsyncComponents: BindingAsyncComponents | null 11 | } 12 | 13 | export class AsyncComponents { 14 | scriptDescriptors: Map = new Map() 15 | jsonAsyncComponentsCache: Map = new Map() 16 | /** 当前状态下热更新时会导致把原有的json内容清除,操作过的page-json需要记录之前的内容 */ 17 | pageJsonCache: Map>> = new Map() 18 | 19 | constructor() {} 20 | 21 | rename = (name: string) => name.startsWith('wx-') ? name.replace('wx-', 'weixin-') : name 22 | 23 | addScriptDescriptor(filename: string, binding: BindingAsyncComponents) { 24 | binding && filename && this.scriptDescriptors.set(filename, { 25 | bindingAsyncComponents: binding, 26 | }) 27 | } 28 | 29 | addAsyncComponents(filename: string, json: BindingAsyncComponents) { 30 | this.jsonAsyncComponentsCache.set(filename, json) 31 | } 32 | 33 | generateBinding(tag: string, path: string, realPath: string) { 34 | return { tag, value: path, type: 'asyncComponent', path: realPath } as const 35 | } 36 | 37 | getComponentPlaceholder(filename: string) { 38 | const cache = this.jsonAsyncComponentsCache.get(filename) 39 | if (!cache) 40 | return null 41 | 42 | const componentPlaceholder = Object.entries(cache).reduce>((p, [key, value]) => { 43 | p[this.rename(key)] = 'view' 44 | return p 45 | }, {}) 46 | return componentPlaceholder 47 | } 48 | 49 | generateComponentPlaceholderJson(filename: string, originJson: Record = {}) { 50 | const componentPlaceholder = this.getComponentPlaceholder(filename) 51 | return Object.assign(originJson || {}, componentPlaceholder || {}) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import Uni from '@dcloudio/vite-plugin-uni' 3 | import Optimization from '@uni-ku/bundle-optimizer' 4 | import UniComponents, { kebabCase } from '@uni-helper/vite-plugin-uni-components' 5 | import { defineConfig } from 'vite' 6 | 7 | export default defineConfig(({ command, mode }) => { 8 | return { 9 | base: './', 10 | plugins: [ 11 | // 可以无需传递任何参数,默认开启所有插件功能,并在项目根目录生成类型定义文件 12 | Optimization({ 13 | enable: { 14 | 'optimization': true, 15 | 'async-import': true, 16 | 'async-component': true, 17 | }, 18 | dts: { 19 | 'enable': true, 20 | 'base': 'src/types', 21 | // 上面是对类型生成的比较全局的一个配置 22 | // 下面是对每个类型生成的配置,以下各配置均为可选参数 23 | 'async-import': { 24 | enable: true, 25 | base: 'src/types', 26 | name: 'async-import.d.ts', 27 | path: 'src/types/async-import.d.ts', 28 | }, 29 | 'async-component': { 30 | enable: true, 31 | base: 'src/types', 32 | name: 'async-component.d.ts', 33 | path: 'src/types/async-component.d.ts', 34 | }, 35 | }, 36 | logger: false, 37 | logToFile: true, 38 | }), 39 | UniComponents({ 40 | dts: 'src/types/components.d.ts', 41 | directoryAsNamespace: true, 42 | resolvers: [ 43 | { 44 | type: 'component', 45 | resolve: (name: string) => { 46 | if (name.match(/^Biz[A-Z]/)) { 47 | const compName = kebabCase(name) 48 | 49 | return { 50 | name, 51 | from: `biz-components/components/${compName}/${compName}.vue`, 52 | } 53 | } 54 | }, 55 | }, 56 | ], 57 | }), 58 | Uni(), 59 | ], 60 | resolve: { 61 | alias: { 62 | '@': fileURLToPath(new URL('./src', import.meta.url)), 63 | }, 64 | }, 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite' 2 | import type { IOptions } from './type' 3 | import fs from 'node:fs' 4 | import { name } from '../package.json' 5 | import { logger } from './common/Logger' 6 | import { ParseOptions } from './common/ParseOptions' 7 | import AsyncComponentProcessor from './plugin/async-component-processor' 8 | import AsyncImportProcessor from './plugin/async-import-processor' 9 | import SubPackagesOptimization from './plugin/subpackages-optimization' 10 | import { ensureDirectoryExists, initializeVitePathResolver } from './utils' 11 | 12 | export default (options: IOptions = {}): PluginOption => { 13 | const parse = new ParseOptions(options) 14 | 15 | let logToFile = options.logToFile 16 | if (logToFile) { 17 | logToFile = typeof logToFile === 'string' ? logToFile : `node_modules/.cache/${name}/logs.log` 18 | if (typeof logToFile !== 'string') { 19 | logger.warn('logToFile should be a string, using default path: node_modules/.cache/bundle-optimizer/logs.log') 20 | logToFile = `node_modules/.cache/${name}/logs.log` 21 | } 22 | ensureDirectoryExists(logToFile) 23 | // 删除旧的日志文件 24 | try { 25 | fs.unlinkSync(logToFile) 26 | } 27 | catch (error) { 28 | logger.error(`Failed to delete old log file: ${error}`) 29 | } 30 | 31 | logger.onLog = (context, level, message, timestamp) => { 32 | const line = `${context} ${level} [${timestamp}]: ${message}` 33 | fs.writeFileSync(logToFile as string, `${line}\n`, { flag: 'a' }) 34 | } 35 | } 36 | 37 | return [ 38 | { 39 | name: 'optimization:initialized', 40 | config(config) { 41 | initializeVitePathResolver(config) 42 | }, 43 | }, 44 | // 分包优化 45 | parse.enable.optimization && SubPackagesOptimization(parse.logger.optimization), 46 | // js/ts插件的异步调用 47 | // 处理 `AsyncImport` 函数调用的路径传参 48 | parse.enable['async-import'] && AsyncImportProcessor(parse.dts['async-import'], parse.logger['async-import']), 49 | // vue组件的异步调用 50 | // 处理 `.vue?async` 查询参数的静态导入 51 | parse.enable['async-component'] && AsyncComponentProcessor(parse.dts['async-component'], parse.logger['async-component']), 52 | ] 53 | } 54 | 55 | export type * from './type' 56 | -------------------------------------------------------------------------------- /src/common/ParseOptions.ts: -------------------------------------------------------------------------------- 1 | import type { Enable, IDtsOptions, IOptions } from '../type' 2 | import { normalizePath } from '../utils' 3 | 4 | export class ParseOptions { 5 | options: IOptions 6 | 7 | constructor(options: IOptions) { 8 | this.options = options 9 | } 10 | 11 | get enable() { 12 | const { enable: origin = true } = this.options 13 | 14 | return typeof origin === 'boolean' 15 | ? { 16 | 'optimization': origin, 17 | 'async-component': origin, 18 | 'async-import': origin, 19 | } 20 | : { 21 | 'optimization': origin.optimization ?? true, 22 | 'async-component': origin['async-component'] ?? true, 23 | 'async-import': origin['async-import'] ?? true, 24 | } 25 | } 26 | 27 | get dts() { 28 | const { dts: origin = true } = this.options 29 | 30 | if (typeof origin === 'boolean') { 31 | return { 32 | 'async-component': this.generateDtsOptions(origin, 'async-component.d.ts'), 33 | 'async-import': this.generateDtsOptions(origin, 'async-import.d.ts'), 34 | } 35 | } 36 | 37 | return { 38 | 'async-component': (origin.enable ?? true) !== false && this.generateDtsOptions(origin['async-component'], 'async-component.d.ts', origin.base), 39 | 'async-import': (origin.enable ?? true) !== false && this.generateDtsOptions(origin['async-import'], 'async-import.d.ts', origin.base), 40 | } 41 | } 42 | 43 | generateDtsOptions(params: boolean | IDtsOptions = true, name: string, base = './') { 44 | if (params === false) 45 | return false 46 | 47 | const path = typeof params === 'boolean' ? `${normalizePath(base).replace(/\/$/, '')}/${name}` : params.enable !== false && normalizePath(params.path || `${normalizePath(params.base ?? base).replace(/\/$/, '')}/${params.name ?? name}`) 48 | return path !== false && { enable: true, path } 49 | } 50 | 51 | get logger() { 52 | const { logger: origin = false } = this.options 53 | const _ = ['optimization', 'async-component', 'async-import'] 54 | const temp = typeof origin === 'boolean' 55 | ? origin ? _ : false 56 | : origin 57 | 58 | return Object.fromEntries(_.map(item => [item, (temp || []).includes(item)])) as Record 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-demo/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 65 | 66 | 87 | -------------------------------------------------------------------------------- /src/utils/visualizer/helper.ts: -------------------------------------------------------------------------------- 1 | import type { GraphRestrictArea } from './type' 2 | 3 | /** 预处理后的数据结构,存储排序后的ID和原始Map */ 4 | interface ProcessedAreaMatcher { 5 | /** ID 列表,按长度从长到短排序 */ 6 | sortedIds: string[] 7 | /** ID 到对象的快速查找 Map (O(1) 查找) */ 8 | areaMap: Map 9 | } 10 | 11 | /** 12 | * 预处理 GraphRestrictArea 数组,用于前缀匹配查找。 13 | * @description 确保 id 唯一性 14 | */ 15 | function preProcessAreasForPrefixMatch(arr: GraphRestrictArea[] = []): ProcessedAreaMatcher { 16 | const areaMap = new Map() 17 | 18 | // O(n) 填充 Map 19 | for (const area of arr) { 20 | areaMap.set(area.id, area) 21 | } 22 | 23 | // O(n log n) 提取 ID 并按长度从长到短排序 (贪婪匹配的关键) 24 | const sortedIds = Array.from(areaMap.keys()).sort((a, b) => b.length - a.length) 25 | 26 | return { sortedIds, areaMap } 27 | } 28 | 29 | /** 基于前缀的匹配 */ 30 | function createCommonMatcher(id: string) { 31 | return (targetString: string) => targetString.startsWith(`${id}/`) 32 | } 33 | 34 | type CreateMatcher = (id: string) => (targetString: string) => T | boolean 35 | 36 | /** 37 | * 使用预处理后的数据结构,通过对应的 matcher 查找匹配的区域。 38 | */ 39 | export function findAreaByMatcher( 40 | processedData: ProcessedAreaMatcher, 41 | targetString?: string, 42 | createMatcher: CreateMatcher = createCommonMatcher, 43 | ): [GraphRestrictArea | undefined, boolean | T | undefined] { 44 | if (targetString === undefined) { 45 | return [undefined, false] 46 | } 47 | // 遍历排序后的 ID (从最长开始) 48 | for (const id of processedData.sortedIds) { 49 | const matcher = createMatcher(id) 50 | const matcheRes = matcher(targetString) 51 | if (matcheRes) { 52 | return [processedData.areaMap.get(id), matcheRes] 53 | } 54 | } 55 | 56 | // 额外的检查:如果目标字符串恰好等于某个 ID (即不是以 / 结尾的路径) 57 | // 例如 ID 是 '/root',目标也是 '/root'。 58 | if (processedData.areaMap.has(targetString)) { 59 | return [processedData.areaMap.get(targetString), true] 60 | } 61 | return [undefined, false] 62 | } 63 | 64 | export function createRestrictAreaSearcher( 65 | restrictAreas?: GraphRestrictArea[], 66 | createMatcher: CreateMatcher = createCommonMatcher, 67 | ) { 68 | const preprocessAreas = preProcessAreasForPrefixMatch(restrictAreas) 69 | return (targetId?: string) => findAreaByMatcher(preprocessAreas, targetId, createMatcher) 70 | } 71 | -------------------------------------------------------------------------------- /src/plugin/vite-plugin-dep-graph.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import type { Plugin } from 'vite' 3 | import { readFileSync } from 'node:fs' 4 | import { writeFile } from 'node:fs/promises' 5 | import path from 'node:path' 6 | import process from 'node:process' 7 | import { setupNunjucks } from '../utils/nunjucks' 8 | import { buildFileSystemTree, transformDataForECharts } from '../utils/visualizer' 9 | 10 | const nunjucks = setupNunjucks() 11 | 12 | export function depGraphPlugin(): Plugin { 13 | const templateFileName = 'template.njk' 14 | // 读取模板文件 15 | const template = readFileSync(path.resolve(__dirname, templateFileName), 'utf-8') 16 | 17 | return { 18 | name: 'vite-plugin-uniapp-dependency-graph', 19 | enforce: 'pre', 20 | 21 | // buildEnd 钩子在每次构建(和重新构建)成功后都会执行 22 | async buildEnd() { 23 | console.log('[DepGraph] Starting analysis after build...') 24 | 25 | const fileSystemTree = buildFileSystemTree(Array.from(this.getModuleIds())) 26 | 27 | /** 28 | * 最外层的基础路径 - 作为节点主要分类 29 | * @TODO: 一次性展示很多节点观感不好,后续会按照文件夹层次展示视图; 30 | * 每一个层次内部节点的分类由该层次的 basePath 标记 31 | */ 32 | const categories = 'basePath' in fileSystemTree ? fileSystemTree.basePath : undefined 33 | 34 | const graphData = transformDataForECharts(this) 35 | graphData.nodes.forEach((node) => { 36 | const targetIndex = categories?.findIndex(category => node.id.startsWith(`${category}/`) || node.id === category) 37 | if (targetIndex !== -1 && targetIndex !== undefined) { 38 | node.category = targetIndex 39 | } 40 | }) 41 | 42 | // 使用 NUNJUCKS 渲染 HTML 43 | const html = nunjucks.renderString(template, { 44 | title: 'Dependency Graph (Static Report)', 45 | isDevServer: false, 46 | dataUrl: '', // 此模式下不需要 47 | dataJsonString: JSON.stringify(Object.assign(graphData, { categories })), 48 | }) 49 | 50 | const outputDir = process.env.UNI_OUTPUT_DIR || path.resolve(process.cwd(), 'dist') 51 | const reportPath = path.resolve(outputDir, 'dependency-graph.html') 52 | 53 | try { 54 | await writeFile(reportPath, html) 55 | console.log('\x1B[32m%s\x1B[0m', ` > Static Dependency Report generated: ${reportPath}`) 56 | } 57 | catch (e) { 58 | this.warn(`Failed to write dependency graph report: ${e}`) 59 | } 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | /* 5+App特有相关 */ 9 | "app-plus": { 10 | "usingComponents": true, 11 | "nvueStyleCompiler": "uni-app", 12 | "compilerVersion": 3, 13 | "splashscreen": { 14 | "alwaysShowBeforeRender": true, 15 | "waiting": true, 16 | "autoclose": true, 17 | "delay": 0 18 | }, 19 | /* 模块配置 */ 20 | "modules": {}, 21 | /* 应用发布信息 */ 22 | "distribute": { 23 | /* android打包配置 */ 24 | "android": { 25 | "permissions": [ 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "" 41 | ] 42 | }, 43 | /* ios打包配置 */ 44 | "ios": {}, 45 | /* SDK配置 */ 46 | "sdkConfigs": {} 47 | } 48 | }, 49 | /* 快应用特有相关 */ 50 | "quickapp": {}, 51 | /* 小程序特有相关 */ 52 | "mp-weixin": { 53 | "appid": "", 54 | "setting": { 55 | "urlCheck": false 56 | }, 57 | "usingComponents": true, 58 | "optimization": { 59 | "subPackages": true 60 | } 61 | }, 62 | "mp-alipay": { 63 | "usingComponents": true 64 | }, 65 | "mp-baidu": { 66 | "usingComponents": true 67 | }, 68 | "mp-toutiao": { 69 | "usingComponents": true 70 | }, 71 | "uniStatistics": { 72 | "enable": false 73 | }, 74 | "vueVersion": "3" 75 | } 76 | -------------------------------------------------------------------------------- /src/common/Logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk' 3 | 4 | enum LogLevel { 5 | DEBUG = 'DEBUG', 6 | INFO = 'INFO', 7 | WARN = 'WARN', 8 | ERROR = 'ERROR', 9 | } 10 | 11 | export class Logger { 12 | private level: LogLevel 13 | private context: string 14 | /** TODO: 可以使用其他的 debug 日志库 */ 15 | private Debugger = null 16 | /** 全局兜底:是否是隐式log */ 17 | private isImplicit: boolean 18 | public onLog?: (context: string, level: `${LogLevel}`, message: string, timestamp: number) => void 19 | 20 | constructor(level: LogLevel = LogLevel.INFO, context: string = 'Plugin', isImplicit = false) { 21 | this.level = level 22 | this.context = context 23 | this.isImplicit = isImplicit 24 | } 25 | 26 | private log(level: LogLevel, message: string, isImplicit?: boolean) { 27 | if (this.shouldLog(level)) { 28 | const coloredMessage = this.getColoredMessage(level, message) 29 | this.onLog?.(this.context, level, message, Date.now()) 30 | if (isImplicit ?? this.isImplicit) { 31 | // TODO: 相关的隐式log,需要通过外部环境变量启用 32 | // 此处暂时不显示 33 | } 34 | else { 35 | const c = 69 36 | const colorCode = `\u001B[3${c < 8 ? c : `8;5;${c}`};1m` 37 | console.log(` ${chalk(`${colorCode}${this.context}`)} ${coloredMessage}`) 38 | } 39 | } 40 | } 41 | 42 | private shouldLog(level: LogLevel): boolean { 43 | const levels: LogLevel[] = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR] 44 | return levels.indexOf(level) >= levels.indexOf(this.level) 45 | } 46 | 47 | private getColoredMessage(level: LogLevel, message: string): string { 48 | switch (level) { 49 | case LogLevel.DEBUG: 50 | return chalk.blue(`[${level}] ${message}`) 51 | case LogLevel.INFO: 52 | return chalk.green(`[${level}] ${message}`) 53 | case LogLevel.WARN: 54 | return chalk.yellow(`[${level}] ${message}`) 55 | case LogLevel.ERROR: 56 | return chalk.red(`[${level}] ${message}`) 57 | default: 58 | return message 59 | } 60 | } 61 | 62 | debug(message: string, isImplicit?: boolean) { 63 | this.log(LogLevel.DEBUG, message, isImplicit) 64 | } 65 | 66 | info(message: string, isImplicit?: boolean) { 67 | this.log(LogLevel.INFO, message, isImplicit) 68 | } 69 | 70 | warn(message: string, isImplicit?: boolean) { 71 | this.log(LogLevel.WARN, message, isImplicit) 72 | } 73 | 74 | error(message: string, isImplicit?: boolean) { 75 | this.log(LogLevel.ERROR, message, isImplicit) 76 | } 77 | } 78 | 79 | export const logger = new Logger(LogLevel.INFO, 'uni-ku:bundle-optimizer') 80 | -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions, IndexHtmlTransformContext, ModuleNode, splitVendorChunk } from 'vite' 2 | 3 | export type Prettify = { 4 | [K in keyof T]: T[K] 5 | } & {} 6 | 7 | // #region Rollup 相关类型定义获取 8 | type ExtractOutputOptions = T extends (infer U)[] ? U : T extends undefined ? never : T 9 | export type OutputOptions = ExtractOutputOptions['output']> 10 | 11 | export type ManualChunksOption = OutputOptions['manualChunks'] 12 | 13 | export type ModuleInfo = Pick< 14 | Exclude, 15 | 'id' | 'meta' | 'importers' | 'importedIds' | 'importedIdResolutions' | 'dynamicImporters' | 'dynamicallyImportedIds' | 'dynamicallyImportedIdResolutions' 16 | > & { isMain?: boolean } 17 | 18 | type GetManualChunk = ReturnType 19 | export type ManualChunkMeta = Parameters['1'] 20 | 21 | export type OutputChunk = Exclude 22 | // #endregion 23 | 24 | export interface ISubPkgsInfo { 25 | root: string 26 | independent: boolean 27 | } 28 | 29 | interface IDtsOptions { 30 | /** 31 | * 是否开启类型定义文件生成(可选) 32 | * 33 | * @description 默认为true,即开启类型定义文件生成 34 | * @default true 35 | */ 36 | enable?: boolean 37 | /** 38 | * 类型定义文件生成的基础路径(可选) 39 | * 40 | * @description 默认为项目根目录 41 | * @description 可以相对路径,也可以绝对路径 42 | * @default './' 43 | */ 44 | base?: string 45 | /** 46 | * 类型定义文件名(可选) 47 | * 48 | * @description 默认为`async-import.d.ts`或`async-component.d.ts` 49 | * @default 'async-import.d.ts' | 'async-component.d.ts' 50 | */ 51 | name?: string 52 | /** 53 | * 类型定义文件生成路径(可选) 54 | * 55 | * @description 默认为`${base}/${name}` 56 | * @description 但是如果指定了此`path`字段,则以`path`为准,优先级更高 57 | * @description 可以相对路径,也可以绝对路径 58 | * @default `${base}/${name}` 59 | */ 60 | path?: string 61 | } 62 | 63 | export type DtsType = false | { enable: boolean, path: string } 64 | 65 | type Enable = 'optimization' | 'async-component' | 'async-import' 66 | 67 | export interface IOptions { 68 | /** 69 | * 插件功能开关(可选) 70 | * 71 | * @description 默认为true,即开启所有功能 72 | */ 73 | enable?: boolean | Prettify>> 74 | /** 75 | * dts文件输出配置(可选) 76 | * 77 | * @description 默认为true,即在项目根目录生成类型定义文件 78 | */ 79 | dts?: Prettify & Record, IDtsOptions | boolean>>> | boolean 80 | /** 81 | * log 控制,默认不启用,为false 82 | */ 83 | logger?: Prettify 84 | /** 85 | * 日志落盘 86 | * --- 87 | * 如果启用则会在 `node_modules/.cache/[项目名称]` 下生成 `logs.log` 日志文件, 也可以传入字符串自定义日志文件路径、名称 88 | */ 89 | logToFile?: boolean | string 90 | } 91 | -------------------------------------------------------------------------------- /.vscode/schema/commitlint-patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "type": "object", 4 | "definitions": { 5 | "rule": { 6 | "description": "A rule", 7 | "type": "array", 8 | "items": [ 9 | { 10 | "description": "Level: 0 disables the rule. For 1 it will be considered a warning, for 2 an error", 11 | "type": "number", 12 | "enum": [0, 1, 2] 13 | }, 14 | { 15 | "description": "Applicable: always|never: never inverts the rule", 16 | "type": "string", 17 | "enum": ["always", "never"] 18 | }, 19 | { 20 | "description": "Value: the value for this rule" 21 | } 22 | ], 23 | "minItems": 1, 24 | "maxItems": 3, 25 | "additionalItems": false 26 | } 27 | }, 28 | "properties": { 29 | "extends": { 30 | "description": "Resolveable ids to commitlint configurations to extend", 31 | "oneOf": [ 32 | { 33 | "type": "array", 34 | "items": { "type": "string" } 35 | }, 36 | { "type": "string" } 37 | ] 38 | }, 39 | "parserPreset": { 40 | "description": "Resolveable id to conventional-changelog parser preset to import and use", 41 | "type": "object", 42 | "properties": { 43 | "name": { "type": "string" }, 44 | "path": { "type": "string" }, 45 | "parserOpts": {} 46 | } 47 | }, 48 | "helpUrl": { 49 | "description": "Custom URL to show upon failure", 50 | "type": "string" 51 | }, 52 | "formatter": { 53 | "description": "Resolveable id to package, from node_modules, which formats the output", 54 | "type": "string" 55 | }, 56 | "rules": { 57 | "description": "Rules to check against", 58 | "type": "object", 59 | "propertyNames": { "type": "string" }, 60 | "additionalProperties": { "$ref": "#/definitions/rule" } 61 | }, 62 | "plugins": { 63 | "description": "Resolveable ids of commitlint plugins from node_modules", 64 | "type": "array", 65 | "items": { 66 | "anyOf": [ 67 | { "type": "string" }, 68 | { 69 | "type": "object", 70 | "required": ["rules"], 71 | "properties": { 72 | "rules": { 73 | "type": "object" 74 | } 75 | } 76 | } 77 | ] 78 | } 79 | }, 80 | "ignores": { 81 | "type": "array", 82 | "items": { "typeof": "function" }, 83 | "description": "Additional commits to ignore, defined by ignore matchers" 84 | }, 85 | "defaultIgnores": { 86 | "description": "Whether commitlint uses the default ignore rules", 87 | "type": "boolean" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uni-ku/bundle-optimizer", 3 | "type": "module", 4 | "version": "1.3.16", 5 | "description": "uni-app 分包优化插件化实现", 6 | "author": { 7 | "name": "Vanisper", 8 | "email": "273266469@qq.com" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/uni-ku/bundle-optimizer#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/uni-ku/bundle-optimizer.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/uni-ku/bundle-optimizer/issues" 18 | }, 19 | "keywords": [ 20 | "Uniapp", 21 | "Vue", 22 | "Vite", 23 | "Vite-Plugin" 24 | ], 25 | "sideEffects": false, 26 | "exports": { 27 | ".": { 28 | "import": "./dist/index.mjs", 29 | "require": "./dist/index.cjs" 30 | } 31 | }, 32 | "main": "dist/index.cjs", 33 | "module": "dist/index.mjs", 34 | "types": "dist/index.d.ts", 35 | "files": [ 36 | "dist" 37 | ], 38 | "publishConfig": { 39 | "access": "public", 40 | "registry": "https://registry.npmjs.org/" 41 | }, 42 | "scripts": { 43 | "build": "unbuild", 44 | "dev": "unbuild --stub", 45 | "release": "npm run build && bumpp", 46 | "prepublishOnly": "npm run build", 47 | "prepare": "simple-git-hooks && npm run build", 48 | "lint": "eslint . --fix", 49 | "commit": "git-cz", 50 | "example1:dev:h5": "npm -C examples/vue3+vite+ts run dev", 51 | "example1:build:h5": "npm -C examples/vue3+vite+ts run build", 52 | "example1:dev:mp-weixin": "npm -C examples/vue3+vite+ts run dev:mp-weixin", 53 | "example1:build:mp-weixin": "npm -C examples/vue3+vite+ts run build:mp-weixin" 54 | }, 55 | "peerDependencies": { 56 | "vite": "^4.0.0 || ^5.0.0" 57 | }, 58 | "dependencies": { 59 | "@dcloudio/uni-cli-shared": "3.0.0-4020820240925001", 60 | "@node-rs/xxhash": "^1.7.6", 61 | "chalk": "4.1.2", 62 | "magic-string": "^0.30.17", 63 | "minimatch": "^9.0.5", 64 | "nunjucks": "^3.2.4" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^4.3.0", 68 | "@commitlint/cli": "^19.3.0", 69 | "@commitlint/config-conventional": "^19.2.2", 70 | "@types/node": "^22.10.2", 71 | "@types/nunjucks": "^3.2.6", 72 | "bumpp": "^9.9.1", 73 | "commitizen": "^4.3.0", 74 | "cz-git": "^1.9.1", 75 | "eslint": "^9.21.0", 76 | "jiti": "^2.4.2", 77 | "lint-staged": "^15.2.2", 78 | "simple-git-hooks": "^2.11.1", 79 | "typescript": "^5.7.3", 80 | "unbuild": "^2.0.0", 81 | "vite": "^4.0.0" 82 | }, 83 | "simple-git-hooks": { 84 | "pre-commit": "pnpm lint-staged", 85 | "commit-msg": "npx commitlint --edit ${1}" 86 | }, 87 | "lint-staged": { 88 | "*.{js,ts,tsx,vue,md}": [ 89 | "eslint . --fix --flag unstable_ts_config" 90 | ] 91 | }, 92 | "config": { 93 | "commitizen": { 94 | "path": "node_modules/cz-git" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/decode-uri-component/index.ts: -------------------------------------------------------------------------------- 1 | const token = '%[a-f0-9]{2}' 2 | const singleMatcher = new RegExp(`(${token})|([^%]+)`, 'gi') 3 | const multiMatcher = new RegExp(`(${token})+`, 'gi') 4 | 5 | function decodeComponents(components: string[], split?: number): string[] { 6 | try { 7 | // Try to decode the entire string first 8 | return [decodeURIComponent(components.join(''))] 9 | } 10 | catch { 11 | // Do nothing 12 | } 13 | 14 | if (components.length === 1) { 15 | return components 16 | } 17 | 18 | split = split || 1 19 | 20 | // Split the array in 2 parts 21 | const left = components.slice(0, split) 22 | const right = components.slice(split) 23 | 24 | return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right)) 25 | } 26 | 27 | function decode(input: string) { 28 | try { 29 | return decodeURIComponent(input) 30 | } 31 | catch { 32 | let tokens = input.match(singleMatcher) || [] 33 | 34 | for (let i = 1; i < tokens.length; i++) { 35 | input = decodeComponents(tokens, i).join('') 36 | 37 | tokens = input.match(singleMatcher) || [] 38 | } 39 | 40 | return input 41 | } 42 | } 43 | 44 | function customDecodeURIComponent(input: string) { 45 | // Keep track of all the replacements and prefill the map with the `BOM` 46 | const replaceMap: Record = { 47 | '%FE%FF': '\uFFFD\uFFFD', 48 | '%FF%FE': '\uFFFD\uFFFD', 49 | } 50 | 51 | let match = multiMatcher.exec(input) 52 | while (match) { 53 | try { 54 | // Decode as big chunks as possible 55 | replaceMap[match[0]] = decodeURIComponent(match[0]) 56 | } 57 | catch { 58 | const result = decode(match[0]) 59 | 60 | if (result !== match[0]) { 61 | replaceMap[match[0]] = result 62 | } 63 | } 64 | 65 | match = multiMatcher.exec(input) 66 | } 67 | 68 | // Add `%C2` at the end of the map to make sure it does not replace the combinator before everything else 69 | replaceMap['%C2'] = '\uFFFD' 70 | 71 | const entries = Object.keys(replaceMap) 72 | 73 | for (const key of entries) { 74 | // Replace all decoded components 75 | input = input.replace(new RegExp(key, 'g'), replaceMap[key]) 76 | } 77 | 78 | return input 79 | } 80 | 81 | /** 82 | * 83 | * @link https://github.com/SamVerschueren/decode-uri-component 84 | */ 85 | export function decodeUriComponent(encodedURI: unknown) { 86 | if (typeof encodedURI !== 'string') { 87 | throw new TypeError(`Expected \`encodedURI\` to be of type \`string\`, got \`${typeof encodedURI}\``) 88 | } 89 | 90 | try { 91 | // Try the built in decoder first 92 | return decodeURIComponent(encodedURI) 93 | } 94 | catch { 95 | // Fallback to a more advanced decoder 96 | return customDecodeURIComponent(encodedURI) 97 | } 98 | } 99 | 100 | export default decodeUriComponent 101 | -------------------------------------------------------------------------------- /src/utils/vite/path-resolver.ts: -------------------------------------------------------------------------------- 1 | import type { Alias, UserConfig } from 'vite' 2 | import path from 'node:path' 3 | import { isRegExp } from 'node:util/types' 4 | import { normalizePath } from '..' 5 | 6 | /** 7 | * @link https://github.com/rollup/plugins/blob/c3dcdc0d2eda4db74bdc772bc369f3f9325802bf/packages/alias/src/index.ts#L7 8 | */ 9 | function matches(pattern: string | RegExp, source: string) { 10 | if (pattern instanceof RegExp) { 11 | return pattern.test(source) 12 | } 13 | if (source.length < pattern.length) { 14 | return false 15 | } 16 | if (source === pattern) { 17 | return true 18 | } 19 | return source.startsWith(pattern.endsWith('/') ? pattern : (`${pattern}/`)) 20 | } 21 | 22 | /** 23 | /** 24 | * 创建一个基于 vite 配置的路径解析函数 25 | * @param config vite 配置 26 | * @returns 路径解析函数 27 | */ 28 | export function createVitePathResolver(config: UserConfig) { 29 | const normalize = (str: any) => { 30 | if (typeof str === 'string' && !isRegExp(str) && !str.includes('*')) { 31 | str = normalizePath(str) 32 | } 33 | return str 34 | } 35 | 36 | const tempAlias = config.resolve?.alias ?? [] 37 | let alias: Alias[] = [] 38 | if (!Array.isArray(tempAlias)) { 39 | alias = Object.entries(tempAlias as { [find: string]: string }).map(([find, replacement]) => ({ find, replacement })) 40 | } 41 | else { 42 | alias = tempAlias 43 | } 44 | 45 | return (source: string, relative = false) => { 46 | const matchedEntry = alias.find(entry => matches(entry.find, source)) 47 | if (!matchedEntry) { 48 | return source 49 | } 50 | const normalizeReplacement = normalize(matchedEntry.replacement) 51 | 52 | if (isRegExp(matchedEntry.find)) { 53 | const realPath = source.replace(matchedEntry.find, normalizeReplacement) 54 | return relative ? realPath : path.resolve(realPath) 55 | } 56 | 57 | // 避开 glob 特征的字符串 58 | if (!matchedEntry.find.includes('*') && !normalizeReplacement.includes('*')) { 59 | // 断定为全量匹配 60 | if (source === matchedEntry.find) { 61 | return relative ? normalizeReplacement : path.resolve(normalizeReplacement) 62 | } 63 | // 断定为前缀匹配 64 | if (source.startsWith(matchedEntry.find)) { 65 | const subPath = source.substring(matchedEntry.find.length) // 获取去除前缀的子串 66 | const realPath = path.join(normalizeReplacement, subPath) // join 自动处理路径拼接问题 67 | return relative ? realPath : path.resolve(realPath) 68 | } 69 | } 70 | return source 71 | } 72 | } 73 | 74 | /** vite插件相关的路径解析 | 单例模式 */ 75 | let vitePathResolver: ((source: string, relative?: boolean) => string) | null = null 76 | 77 | export function getVitePathResolver() { 78 | if (!vitePathResolver) { 79 | throw new Error('Vite path resolver has not been initialized. Please call createVitePathResolver first.') 80 | } 81 | return vitePathResolver 82 | } 83 | 84 | export function initializeVitePathResolver(config: UserConfig) { 85 | vitePathResolver = createVitePathResolver(config) 86 | } 87 | -------------------------------------------------------------------------------- /src/utils/lex-parse/parse_import.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-cond-assign */ 2 | import type { ImportDefaultWithQuery } from './type' 3 | 4 | export const IMPORT_DEFAULT_WITH_QUERY_RE = /import\s+(\w+)\s+from\s+(['"])([^'"]+)\?(\w+(?:&\w+)*)\2(?:\s*;)?/g 5 | function parseValue(value: string) { 6 | if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"'))) { 7 | return value.slice(1, -1) // 移除引号 8 | } 9 | return value 10 | } 11 | 12 | export function lexDefaultImportWithQuery(code: string) { 13 | const matches: ImportDefaultWithQuery[] = [] 14 | let match: RegExpExecArray | null 15 | 16 | // 逐个匹配函数调用 17 | while ((match = IMPORT_DEFAULT_WITH_QUERY_RE.exec(code)) !== null) { 18 | const fullMatchLocation = { 19 | start: match.index, // 函数调用的起始位置 20 | end: match.index + match[0].length, // 函数调用的结束位置 21 | fullMatch: match[0], // 完整匹配的函数调用 22 | } 23 | 24 | const defaultVariable = { 25 | value: parseValue(match[1]), 26 | start: match.index + match[0].indexOf(match[1]), 27 | end: match.index + match[0].indexOf(match[1]) + match[1].length, 28 | } 29 | 30 | const modulePath = { 31 | value: parseValue(match[3]), 32 | start: match.index + match[0].indexOf(match[3]), 33 | end: match.index + match[0].indexOf(match[3]) + match[3].length, 34 | } 35 | 36 | /** 字符的长度加上一个`&`或者`?`的长度 */ 37 | let lastLength = 0 38 | const query = match[4].split('&').map((queryParam, _index, list) => { 39 | lastLength += (list[_index - 1]?.length || 0) + 1 40 | 41 | const prevLength = modulePath.end + lastLength 42 | const start = prevLength + match![0].slice(prevLength - fullMatchLocation.start).indexOf(queryParam) 43 | const end = start + queryParam.length 44 | 45 | return { 46 | value: parseValue(queryParam), 47 | start, 48 | end, 49 | } 50 | }) 51 | 52 | const fullPath = { 53 | value: parseValue(`${match[3]}?${match[4]}`), 54 | start: modulePath.start, 55 | end: query[query.length - 1].end, 56 | } 57 | 58 | matches.push({ 59 | full: fullMatchLocation, // 完整匹配的函数调用 60 | defaultVariable, // 参数解析结果 61 | modulePath, 62 | query, 63 | fullPath, // 完整路径信息 64 | }) 65 | } 66 | 67 | return matches 68 | } 69 | 70 | // 测试用例 71 | // const code = ` 72 | // import type { ParseError as EsModuleLexerParseError, ImportSpecifier } from 'es-module-lexer' 73 | // import type { Plugin } from 'vite'; 74 | // import type { IOptimizationOptions } from './type' 75 | // import { init, parse as parseImports } from 'es-module-lexer' 76 | // import MagicString from 'magic-string' 77 | // import { createParseErrorInfo, parseImportDefaultWithQuery } from '../utils' 78 | // import AsyncImport from './async-import?a' 79 | // import UniappSubPackagesOptimization from './main' 80 | // ` 81 | 82 | // console.log(code.slice(lexDefaultImportWithQuery(code)[0].full.start, lexDefaultImportWithQuery(code)[0].full.end).endsWith('\n')) 83 | -------------------------------------------------------------------------------- /examples/hbx+vue3/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "demo", 3 | "appid" : "__UNI__BB80E5C", 4 | "description" : "", 5 | "versionName" : "1.0.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | /* 5+App特有相关 */ 9 | "app-plus" : { 10 | "usingComponents" : true, 11 | "nvueStyleCompiler" : "uni-app", 12 | "compilerVersion" : 3, 13 | "splashscreen" : { 14 | "alwaysShowBeforeRender" : true, 15 | "waiting" : true, 16 | "autoclose" : true, 17 | "delay" : 0 18 | }, 19 | /* 模块配置 */ 20 | "modules" : {}, 21 | /* 应用发布信息 */ 22 | "distribute" : { 23 | /* android打包配置 */ 24 | "android" : { 25 | "permissions" : [ 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "" 41 | ] 42 | }, 43 | /* ios打包配置 */ 44 | "ios" : {}, 45 | /* SDK配置 */ 46 | "sdkConfigs" : {} 47 | } 48 | }, 49 | /* 快应用特有相关 */ 50 | "quickapp" : {}, 51 | /* 小程序特有相关 */ 52 | "mp-weixin" : { 53 | "appid" : "", 54 | "setting" : { 55 | "urlCheck" : false 56 | }, 57 | "usingComponents" : true, 58 | "optimization": { 59 | "subPackages": true 60 | } 61 | }, 62 | "mp-alipay" : { 63 | "usingComponents" : true 64 | }, 65 | "mp-baidu" : { 66 | "usingComponents" : true 67 | }, 68 | "mp-toutiao" : { 69 | "usingComponents" : true 70 | }, 71 | "uniStatistics" : { 72 | "enable" : false 73 | }, 74 | "vueVersion" : "3" 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/lex-parse/parse_arguments.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | /* eslint-disable no-cond-assign */ 3 | 4 | import type { ArgumentLocation, FullMatchLocation, FunctionCall } from './type' 5 | 6 | export function lexFunctionCalls(code: string, functionName: string) { 7 | // 正则匹配指定函数名的调用,并提取参数部分 8 | const functionPattern = new RegExp( 9 | `\\b${functionName}\\s*\\(\\s*([^\\)]*)\\s*\\)`, // 函数名 + 参数部分 10 | 'g', 11 | ) 12 | 13 | const matches: FunctionCall[] = [] 14 | let match: RegExpExecArray | null 15 | 16 | // 逐个匹配函数调用 17 | while ((match = functionPattern.exec(code)) !== null) { 18 | // 提取参数部分 19 | const argsString = match[1] 20 | 21 | const fullMatchLocation: FullMatchLocation = { 22 | start: match.index, // 函数调用的起始位置 23 | end: match.index + match[0].length, // 函数调用的结束位置 24 | fullMatch: match[0], // 完整匹配的函数调用 25 | } 26 | 27 | // 计算函数名+括号的结束位置偏移量,用于修正参数的起始位置 28 | const functionCallPrefixEnd = match.index + match[0].indexOf('(') + 1 29 | const padStartCount = match[0].indexOf(argsString) - (match[0].indexOf('(') + 1) 30 | 31 | // 解析函数的参数及其定位 32 | const args = parseArguments(argsString.padStart(argsString.length + padStartCount), functionCallPrefixEnd, code) 33 | 34 | matches.push({ 35 | full: fullMatchLocation, // 完整匹配的函数调用 36 | args, // 参数解析结果 37 | }) 38 | } 39 | 40 | return matches 41 | } 42 | 43 | function parseArguments(argsString: string, functionPrefixEnd: number, code: string): ArgumentLocation[] { 44 | const args: ArgumentLocation[] = [] 45 | 46 | // 匹配字符串、数字和变量 47 | const argPattern = /'(?:\\'|[^'])*'|"(?:\\"|[^"])*"|\d+(?:\.\d+)?|\w+/g // 48 | 49 | let match: RegExpExecArray | null 50 | while ((match = argPattern.exec(argsString)) !== null) { 51 | // 获取匹配的参数值 52 | const argValue = match[0] 53 | const argStart = functionPrefixEnd + argsString.slice(0, match.index).length // 参数起始位置 54 | const argEnd = argStart + argValue.length // 参数结束位置 55 | 56 | // 去掉字符串中的引号 57 | let value: ArgumentLocation['value'] = argValue 58 | if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"'))) { 59 | value = value.slice(1, -1) // 移除引号 60 | } 61 | else if (!Number.isNaN(Number(value))) { 62 | value = Number(value) // 将数字字符串转换为数字 63 | } 64 | 65 | args.push({ 66 | value, // 只保留实际的参数值 67 | start: argStart, // 修正后的起始位置 68 | end: argEnd, // 修正后的结束位置 69 | }) 70 | } 71 | 72 | return args 73 | } 74 | 75 | // // 测试代码 76 | // const code = ` 77 | // AsyncImport('module1'); 78 | // AsyncImport("module2", "module3"); 79 | // AsyncImport('module4', 'module5'); 80 | // AsyncImport('module6', "module7", 'module8'); 81 | // CustomFunction( 'arg1' , 123 , varName ) ; 82 | // ` 83 | 84 | // const asyncImports = lexFunctionCalls(code, "AsyncImport") 85 | // // console.log('AsyncImports:', JSON.stringify(asyncImports, null, 2)) 86 | 87 | // const customFunctions = lexFunctionCalls(code, "CustomFunction") 88 | // // console.log("CustomFunctions:", JSON.stringify(customFunctions, null, 2)) 89 | -------------------------------------------------------------------------------- /src/utils/uniapp/index.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleInfo } from '../../type' 2 | import { moduleIdProcessor, parseQuerystring } from '..' 3 | import { ROOT_DIR } from '../../constants' 4 | import base64url from '../base64url' 5 | 6 | export const uniPagePrefix = 'uniPage://' as const 7 | export const uniComponentPrefix = 'uniComponent://' as const 8 | 9 | export function virtualPagePath(filepath: string): `${typeof uniPagePrefix}${string}` { 10 | return `${uniPagePrefix}${base64url.encode(filepath)}` 11 | } 12 | 13 | export function virtualComponentPath(filepath: string): `${typeof uniComponentPrefix}${string}` { 14 | return `${uniComponentPrefix}${base64url.encode(filepath)}` 15 | } 16 | 17 | export function parseVirtualPagePath(uniPageUrl: string) { 18 | return base64url.decode(uniPageUrl.replace(uniPagePrefix, '')) 19 | } 20 | 21 | export function parseVirtualComponentPath(uniComponentUrl: string) { 22 | return base64url.decode(uniComponentUrl.replace(uniComponentPrefix, '')) 23 | } 24 | 25 | export function isUniVirtualPagePath(path: string): path is `${typeof uniPagePrefix}${string}` { 26 | return path.startsWith(uniPagePrefix) 27 | } 28 | 29 | export function isUniVirtualComponentPath(path: string): path is `${typeof uniComponentPrefix}${string}` { 30 | return path.startsWith(uniComponentPrefix) 31 | } 32 | 33 | export function isUniVirtualPath(path: string): path is `${typeof uniPagePrefix}${string}` | `${typeof uniComponentPrefix}${string}` { 34 | return isUniVirtualPagePath(path) || isUniVirtualComponentPath(path) 35 | } 36 | 37 | // Old: [boolean, string, 'page' | 'component' | null] 38 | type ParseResult = 39 | | [true, string, 'page' | 'component'] 40 | | [false, string, null] 41 | 42 | export function parseVirtualPath(virtualUrl?: T): ParseResult { 43 | if (virtualUrl?.startsWith(uniPagePrefix)) { 44 | return [true, parseVirtualPagePath(virtualUrl), 'page'] 45 | } 46 | if (virtualUrl?.startsWith(uniComponentPrefix)) { 47 | return [true, parseVirtualComponentPath(virtualUrl), 'component'] 48 | } 49 | return [false, virtualUrl ?? '', null] 50 | } 51 | 52 | /** 53 | * 创建一个 vue 文件的 script 函数模块解析函数 54 | * @example 类似于 `xxx.vue?vue&type=script&setup=true&lang.ts` 的路径 55 | */ 56 | export function createVueScriptAnalysis(inputDir = ROOT_DIR) { 57 | /** 58 | * # id处理器 59 | * @description 将id中的moduleId转换为相对于inputDir的路径并去除查询参数后缀 60 | */ 61 | function _moduleIdProcessor(id: string, removeQuery = true) { 62 | return moduleIdProcessor(id, inputDir, removeQuery) 63 | } 64 | 65 | /** 66 | * 判断模块是否是一个 vue 文件的 script 函数模块 67 | * @example 类似于 `xxx.vue?vue&type=script&setup=true&lang.ts` 的路径 68 | */ 69 | return function isVueScript(moduleInfo?: Partial | null): moduleInfo is Partial { 70 | if (!moduleInfo?.id || !('importers' in moduleInfo) || !moduleInfo?.importers?.length) { 71 | return false 72 | } 73 | const importer = _moduleIdProcessor(moduleInfo.importers[0]) 74 | const id = moduleInfo.id 75 | const clearId = _moduleIdProcessor(id, false) 76 | 77 | const parsedUrl = parseQuerystring(clearId) 78 | 79 | return !!parsedUrl && parsedUrl.type === 'script' && parsedUrl.vue === true && importer === _moduleIdProcessor(id) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.commitlintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | 4 | # https://github.com/conventional-changelog/conventional-changelog/issues/234#issuecomment-766839160 5 | # https://github.com/ccnnde/commitlint-config-git-commit-emoji/blob/master/index.js#L4 6 | parserPreset: 7 | parserOpts: 8 | headerPattern: ^((?(?::\w*:|\ud83c[\udde0-\uddff]|\ud83c[\udf00-\udfff]|\ud83d[\ude00-\ude4f]|\ud83d[\ude80-\udeff]|\ud83d[\udf00-\udf7f]|\ud83d[\udf80-\udfff]|\ud83e[\udc00-\udcff]|\ud83e[\udd00-\uddff]|\ud83e[\ude00-\ude6f]|\ud83e[\ude70-\udeff]|[\u2600-\u2B55]))\s)?(?\w+)(?:\((?[^)]*)\))?!?:\s((?(?::\w*:|\ud83c[\udde0-\uddff]|\ud83c[\udf00-\udfff]|\ud83d[\ude00-\ude4f]|\ud83d[\ude80-\udeff]|\ud83d[\udf00-\udf7f]|\ud83d[\udf80-\udfff]|\ud83e[\udc00-\udcff]|\ud83e[\udd00-\uddff]|\ud83e[\ude00-\ude6f]|\ud83e[\ude70-\udeff]|[\u2600-\u2B55]))\s)?(?(?:(?!#).)*(?:(?!\s).))(?:\s(?#(?\w+)|\(#(?\w+)\)))?(?:\s(?(?::\w*:|\ud83c[\udde0-\uddff]|\ud83c[\udf00-\udfff]|\ud83d[\ude00-\ude4f]|\ud83d[\ude80-\udeff]|\ud83d[\udf00-\udf7f]|\ud83d[\udf80-\udfff]|\ud83e[\udc00-\udcff]|\ud83e[\udd00-\uddff]|\ud83e[\ude00-\ude6f]|\ud83e[\ude70-\udeff]|[\u2600-\u2B55])))?$ 9 | headerCorrespondence: [emoji_left_, emoji_left, type, scope, emoji_center_, emoji_center, subject, ticket, ticket_number1, ticket_number2, emoji_right] 10 | 11 | rules: 12 | type-enum: 13 | - 2 14 | - always 15 | - 16 | - feat 17 | - perf 18 | - fix 19 | - refactor 20 | - docs 21 | - build 22 | - types 23 | - chore 24 | - examples 25 | - test 26 | - style 27 | - ci 28 | - init 29 | prompt: 30 | messages: 31 | type: '选择你要提交的类型 :' 32 | scope: '选择一个提交范围 (可选) :' 33 | customScope: '请输入自定义的提交范围 :' 34 | subject: "填写简短精炼的变更描述 :\n" 35 | body: "填写更加详细的变更描述 (可选) 。使用 \"|\" 换行 :\n" 36 | breaking: "列举非兼容性重大的变更 (可选) 。使用 \"|\" 换行 :\n" 37 | footerPrefixesSelect: '设置关联issue前缀 (可选) :' 38 | customFooterPrefix: '输入自定义issue前缀 :' 39 | footer: "列举关联issue (可选) 例如: #1 :\n" 40 | confirmCommit: 是否提交或修改commit ? 41 | types: 42 | - value: feat 43 | name: '🚀 Features: 新功能' 44 | emoji: 🚀 45 | - value: perf 46 | name: '🔥 Performance: 性能优化' 47 | emoji: 🔥 48 | - value: fix 49 | name: '🩹 Fixes: 缺陷修复' 50 | emoji: 🩹 51 | - value: refactor 52 | name: '💅 Refactors: 代码重构' 53 | emoji: 💅 54 | - value: docs 55 | name: '📖 Documentation: 文档' 56 | emoji: 📖 57 | - value: build 58 | name: '📦 Build: 构建工具' 59 | emoji: 📦 60 | - value: types 61 | name: '🌊 Types: 类型定义' 62 | emoji: 🌊 63 | - value: chore 64 | name: '🏡 Chore: 简修处理' 65 | emoji: 🏡 66 | - value: examples 67 | name: '🏀 Examples: 例子展示' 68 | emoji: 🏀 69 | - value: test 70 | name: '✅ Tests: 测试用例' 71 | emoji: ✅ 72 | - value: style 73 | name: '🎨 Styles: 代码风格' 74 | emoji: 🎨 75 | - value: ci 76 | name: '🤖 CI: 持续集成' 77 | emoji: 🤖 78 | - value: init 79 | name: '🎉 Init: 项目初始化' 80 | emoji: 🎉 81 | useEmoji: true 82 | emojiAlign: left 83 | scopes: [] 84 | maxHeaderLength: 72 85 | -------------------------------------------------------------------------------- /.vscode/schema/commitlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "type": "object", 4 | "definitions": { 5 | "rule": { 6 | "oneOf": [ 7 | { 8 | "description": "A rule", 9 | "type": "array", 10 | "items": [ 11 | { 12 | "description": "Level: 0 disables the rule. For 1 it will be considered a warning, for 2 an error", 13 | "type": "number", 14 | "enum": [0, 1, 2] 15 | }, 16 | { 17 | "description": "Applicable: always|never: never inverts the rule", 18 | "type": "string", 19 | "enum": ["always", "never"] 20 | }, 21 | { 22 | "description": "Value: the value for this rule" 23 | } 24 | ], 25 | "minItems": 1, 26 | "maxItems": 3, 27 | "additionalItems": false 28 | }, 29 | { 30 | "description": "A rule", 31 | "typeof": "function" 32 | } 33 | ] 34 | } 35 | }, 36 | "properties": { 37 | "extends": { 38 | "description": "Resolveable ids to commitlint configurations to extend", 39 | "oneOf": [ 40 | { 41 | "type": "array", 42 | "items": { "type": "string" } 43 | }, 44 | { "type": "string" } 45 | ] 46 | }, 47 | "parserPreset": { 48 | "description": "Resolveable id to conventional-changelog parser preset to import and use", 49 | "oneOf": [ 50 | { "type": "string" }, 51 | { 52 | "type": "object", 53 | "properties": { 54 | "name": { "type": "string" }, 55 | "path": { "type": "string" }, 56 | "parserOpts": {} 57 | }, 58 | "additionalProperties": true 59 | }, 60 | { "typeof": "function" } 61 | ] 62 | }, 63 | "helpUrl": { 64 | "description": "Custom URL to show upon failure", 65 | "type": "string" 66 | }, 67 | "formatter": { 68 | "description": "Resolveable id to package, from node_modules, which formats the output", 69 | "type": "string" 70 | }, 71 | "rules": { 72 | "description": "Rules to check against", 73 | "type": "object", 74 | "propertyNames": { "type": "string" }, 75 | "additionalProperties": { "$ref": "#/definitions/rule" } 76 | }, 77 | "plugins": { 78 | "description": "Resolveable ids of commitlint plugins from node_modules", 79 | "type": "array", 80 | "items": { 81 | "anyOf": [ 82 | { "type": "string" }, 83 | { 84 | "type": "object", 85 | "required": ["rules"], 86 | "properties": { 87 | "rules": { 88 | "type": "object" 89 | } 90 | } 91 | } 92 | ] 93 | } 94 | }, 95 | "ignores": { 96 | "type": "array", 97 | "items": { "typeof": "function" }, 98 | "description": "Additional commits to ignore, defined by ignore matchers" 99 | }, 100 | "defaultIgnores": { 101 | "description": "Whether commitlint uses the default ignore rules", 102 | "type": "boolean" 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/utils/visualizer/file-system.ts: -------------------------------------------------------------------------------- 1 | import { createEdgeRegex } from '..' 2 | 3 | interface FileSystemNode { 4 | id: string 5 | name: string 6 | isDir: boolean 7 | children?: FileSystemNode[] 8 | /** 权重值 */ 9 | value?: number 10 | } 11 | 12 | interface FlattenedFileSystemNode extends FileSystemNode { 13 | /** 经过扁平化处理后,当前节点下第一层级的子节点ID列表 */ 14 | basePath?: string[] 15 | } 16 | 17 | /** 18 | * 将一个文件系统节点(及其子树)进行单层目录合并 19 | * @description 这是一个可重用的函数,可以对树中的任何节点调用 20 | * @param node 要进行扁平化处理的节点 21 | * @param pathSeparator 路径分隔符,默认为'/' 22 | * @returns 返回一个新的扁平化后的节点,并包含一个 `basePath` 属性 23 | */ 24 | export function flattenNode(node: FileSystemNode, pathSeparator: string = '/'): FlattenedFileSystemNode { 25 | // 如果不是目录或没有子节点,则无需处理,直接返回。 26 | // basePath 为空,因为它没有子节点。 27 | if (!node.isDir || !node.children || node.children.length === 0) { 28 | return { ...node } 29 | } 30 | 31 | // 递归处理所有子节点 (后序遍历) 32 | // 确保所有子树都已经被扁平化处理 33 | const flattenedChildren = node.children.map(child => flattenNode(child, pathSeparator)) 34 | 35 | // 对当前节点应用合并逻辑 36 | // 使用处理过的子节点来更新当前节点 37 | let resultNode: FileSystemNode = { 38 | ...node, 39 | children: flattenedChildren, 40 | } 41 | 42 | // 循环合并:只要当前节点仍然是只有一个子目录的目录,就继续合并 43 | while (resultNode.children?.length === 1 && resultNode.children[0].isDir) { 44 | const singleChild = resultNode.children[0] 45 | 46 | // 合并名称和ID,并直接继承孙子节点 47 | resultNode = { 48 | ...resultNode, 49 | name: `${resultNode.name}${pathSeparator}${singleChild.name}`, 50 | id: singleChild.id, // ID 更新为最深的子节点的ID 51 | children: singleChild.children, 52 | } 53 | } 54 | 55 | // 计算 basePath 56 | // basePath 的定义是:当前扁平化操作完成后,该节点下第一层子节点的 ID 列表。 57 | const basePath = resultNode.children?.map(child => child.id) ?? [] 58 | 59 | return { 60 | ...resultNode, 61 | basePath, 62 | } 63 | } 64 | 65 | /** 66 | * 将文件路径列表转换为树形的文件系统数据结构,可选择合并单层目录 67 | * @param paths 路径字符串数组 68 | * @param flattenSingleDir 是否合并只有一个子文件夹的目录(默认为 true) 69 | * @returns 根节点对象 70 | */ 71 | export function buildFileSystemTree(paths: string[], flattenSingleDir: boolean = true): FileSystemNode | FlattenedFileSystemNode { 72 | const separator = '/' 73 | const root: FileSystemNode = { 74 | id: separator, 75 | name: separator, 76 | isDir: true, 77 | children: [], 78 | } 79 | 80 | for (const p of paths) { 81 | // 统一处理路径,移除开头和结尾的斜杠 82 | // const parts = p.startsWith(separator) ? p.substring(1).split(separator) : p.split(separator); 83 | const parts = p.replace(createEdgeRegex(separator), '').split(separator) 84 | 85 | const isSeparatorPrefixed = p.startsWith(separator) 86 | let currentNode = root 87 | 88 | for (let i = 0; i < parts.length; i++) { 89 | const name = parts[i] 90 | // 需要兼容虚拟路径的情况,类似 `uniPage://`、`uniComponent://`,所以取消以下操作 91 | // if (!name) continue; // 忽略像 "a//b" 这样的空部分 92 | 93 | let foundNode = currentNode.children?.find(node => node.name === name) 94 | 95 | if (!foundNode) { 96 | // 归还原本就是以 separator 为前缀的路径 97 | const id = (isSeparatorPrefixed ? separator : '') + parts.slice(0, i + 1).join(separator) 98 | const isDir = i < parts.length - 1 99 | const children = isDir ? [] : undefined 100 | 101 | foundNode = { id, name, isDir, children } 102 | 103 | // 保证文件夹在前,文件在后 104 | if (isDir) { 105 | const lastDirIndex = currentNode.children?.map(n => n.isDir).lastIndexOf(true) ?? -1 106 | currentNode.children?.splice(lastDirIndex + 1, 0, foundNode) 107 | } 108 | else { 109 | currentNode.children?.push(foundNode) 110 | } 111 | } 112 | currentNode = foundNode 113 | } 114 | } 115 | 116 | // 扁平化 117 | if (flattenSingleDir) { 118 | return flattenNode(root, separator) 119 | } 120 | 121 | return root 122 | } 123 | -------------------------------------------------------------------------------- /src/plugin/vite-plugin-global-method.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/no-unsafe-function-type */ 2 | 3 | import type { Plugin } from 'vite' 4 | import fs from 'node:fs' 5 | import path from 'node:path' 6 | import MagicString from 'magic-string' 7 | import { minimatch } from 'minimatch' 8 | import { ROOT_DIR } from '../constants' 9 | import { ensureDirectoryExists, normalizeFunctionSyntax } from '../utils' 10 | 11 | /** 12 | * 全局方法注入 13 | * 14 | * @description 通过`globalThis`对象注册全局方法,支持生成类型文件 15 | */ 16 | export function GlobalMethodPlugin(options: GlobalMethodOptions): Plugin { 17 | const { 18 | methods, 19 | include = [], 20 | exclude = [], 21 | rootDir = ROOT_DIR, 22 | generateTypes = true, // 默认为生成类型文件 23 | typesFilePath = path.resolve(ROOT_DIR, 'global-method.d.ts'), // 默认为 global-method.d.ts 24 | } = options 25 | 26 | // 文件匹配函数 27 | const filter = (id: string) => { 28 | const relativePath = path.relative(rootDir, id).replace(/\\/g, '/') 29 | const isIncluded = include.length === 0 || include.some(pattern => minimatch(relativePath, pattern)) 30 | const isExcluded = exclude.length > 0 && exclude.some(pattern => minimatch(relativePath, pattern)) 31 | return isIncluded && !isExcluded 32 | } 33 | 34 | // 生成批量注册方法的代码 35 | const globalMethodsCode = ` 36 | if (typeof globalThis._globalMethods === 'undefined') { 37 | globalThis._globalMethods = {}; 38 | ${Object.entries(methods).map(([methodName, methodBody]) => { 39 | const targetMethodBody = Array.isArray(methodBody) ? methodBody[0] : methodBody 40 | 41 | const methodCode = typeof targetMethodBody === 'string' 42 | ? `function() { ${targetMethodBody} }` 43 | : typeof targetMethodBody === 'function' ? normalizeFunctionSyntax(targetMethodBody.toString(), true) : '' 44 | 45 | return methodCode && ` 46 | if (typeof globalThis.${methodName} === 'undefined') { 47 | globalThis.${methodName} = ${methodCode}; 48 | } 49 | ` 50 | }).join('')} 51 | } 52 | ` 53 | 54 | // 生成类型声明的代码 55 | const generateTypesCode = `export {} 56 | 57 | declare global {${Object.entries(methods).map(([methodName, methodBody]) => { 58 | const methodInterface = Array.isArray(methodBody) 59 | ? methodBody[1] 60 | : (typeof methodBody === 'string' || typeof methodBody === 'function') ? {} : methodBody 61 | 62 | return ` 63 | ${generateFunctionType(methodName, methodInterface)}` 64 | }, 65 | ).join('') 66 | } 67 | } 68 | ` 69 | 70 | return { 71 | name: 'vite-plugin-global-methods', 72 | enforce: 'post', // 插件执行时机,在其他处理后执行 73 | 74 | transform(code, id) { 75 | if (!filter(id)) 76 | return null 77 | 78 | const magicString = new MagicString(code) 79 | magicString.prepend(globalMethodsCode) 80 | 81 | // 如果需要生成类型文件 82 | if (generateTypes) { 83 | ensureDirectoryExists(typesFilePath) 84 | fs.writeFileSync(typesFilePath, generateTypesCode) 85 | } 86 | 87 | return { 88 | code: magicString.toString(), 89 | map: magicString.generateMap({ hires: true }), 90 | } 91 | }, 92 | } 93 | } 94 | 95 | /** 生成函数的ts接口类型 */ 96 | function generateFunctionType(funcName: string, { paramsType, returnType }: FunctionInterface = {}) { 97 | const params = Array.isArray(paramsType) ? paramsType.map((item, index) => `arg${index}: ${item}`).join(', ') : `arg: ${paramsType || 'any'}` 98 | return `function ${funcName}(${params}): ${returnType || 'any'}` 99 | } 100 | 101 | interface FunctionInterface { 102 | /** 函数入参类型数组 */ 103 | paramsType?: string | string[] 104 | /** 函数返回值类型 */ 105 | returnType?: string 106 | } 107 | 108 | interface GlobalMethodMap { 109 | [methodName: string]: string | Function 110 | | [string | Function] 111 | | [string | Function, FunctionInterface] 112 | | FunctionInterface // 仅仅生成类型声明 113 | } 114 | 115 | export type GlobalMethod = GlobalMethodMap 116 | 117 | export interface GlobalMethodOptions { 118 | methods: GlobalMethod 119 | include?: string[] 120 | exclude?: string[] 121 | rootDir?: string 122 | /** 是否生成 TS 类型声明 | 默认为 true */ 123 | generateTypes?: boolean 124 | /** 生成的类型声明文件路径 | 默认项目根目录下`global-method.d.ts` */ 125 | typesFilePath?: string 126 | } 127 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import process from 'node:process' 4 | import { ASSETS_DIR_RE, EXT_RE, ROOT_DIR, SRC_DIR_RE } from '../constants' 5 | import * as querystring from './query-string' 6 | 7 | /** 替换字符串指定位置的字符 */ 8 | export function replaceStringAtPosition(originalStr: string, start: number, end: number, replaceWith: string) { 9 | return originalStr.substring(0, start) + replaceWith + originalStr.substring(end) 10 | } 11 | 12 | /** 转换为斜杠路径 */ 13 | export function slash(p: string): string { 14 | return p.replace(/\\/g, '/') 15 | } 16 | 17 | /** 规范路径 | 处理路径斜杠 */ 18 | export function normalizePath(id: string) { 19 | return process.platform === 'win32' ? slash(id) : id 20 | } 21 | 22 | /** 规范函数语法 */ 23 | export function normalizeFunctionSyntax(funcStr: string, anonymous = false): string { 24 | return funcStr.replace(/^\s*(async\s+)?(function\s+)?([\w$]+)\s*\(/, (match, asyncKeyword, funcKeyword, funcName) => { 25 | return !anonymous && funcName && !['function', 'async'].includes(funcName) 26 | ? `${asyncKeyword || ''}function ${funcName}(` 27 | : `${asyncKeyword || ''}${funcName === 'async' ? padEndStringSpaces(funcName, 1) : 'function'}(` 28 | }) 29 | } 30 | 31 | /** 32 | * 字符串末尾填充空格 33 | * @param str 待处理字符串 34 | * @param count 填充数量 | 默认 0 35 | * @returns 处理后的字符串 | 兜底处理成空字符串 36 | */ 37 | export function padEndStringSpaces(str: string | undefined, count = 0) { 38 | str = str?.toString() 39 | return str?.padEnd(str?.length + count) || '' 40 | } 41 | 42 | /** 检查并创建目录 */ 43 | export function ensureDirectoryExists(filePath: string) { 44 | const dir = path.dirname(filePath) 45 | if (!fs.existsSync(dir)) { 46 | fs.mkdirSync(dir, { recursive: true }) 47 | } 48 | } 49 | 50 | /** 路径处理器 | 去除`rootDir`前缀路径和查询参数 | `rootDir`默认为项目根目录 */ 51 | export function moduleIdProcessor(id: string, rootDir = ROOT_DIR, removeQuery = true) { 52 | rootDir = normalizePath(rootDir) 53 | // 确保 rootDir 以斜杠结尾 54 | if (!rootDir.endsWith('/')) 55 | rootDir += '/' 56 | 57 | const normalized = normalizePath(id) 58 | const name = removeQuery ? normalized.split('?')[0] : normalized 59 | // 从name中剔除 rootDir 前缀 60 | const updatedName = name.replace(rootDir, '') 61 | 62 | // 去除来自`node_modules`模块的前缀 63 | if (updatedName.startsWith('\x00')) 64 | return updatedName.slice(1) 65 | 66 | return updatedName 67 | } 68 | 69 | /** 70 | * 计算相对路径的调用层级 71 | * @param importer 引入者文件的路径 72 | * @param imported 被引入文件的路径 73 | * @returns 相对路径前缀 74 | */ 75 | export function calculateRelativePath(importer: string, imported: string): string { 76 | // 获取相对路径 77 | if (imported.match(/^(\.\/|\.\.\/)+/)) { 78 | imported = path.resolve(path.dirname(importer), imported) 79 | } 80 | const relativePath = path.relative(path.dirname(importer), imported) 81 | 82 | // 将路径中的反斜杠替换为正斜杠(适用于 Windows 系统) 83 | return relativePath.replace(/\\/g, '/') 84 | } 85 | 86 | /** 处理 src 前缀的路径 */ 87 | export function resolveSrcPath(id: string) { 88 | return id.replace(SRC_DIR_RE, './') 89 | } 90 | 91 | /** 处理 assets 前缀的路径 */ 92 | export function resolveAssetsPath(id: string) { 93 | return id.replace(ASSETS_DIR_RE, './') 94 | } 95 | 96 | /** 判断是否有后缀 */ 97 | export function hasExtension(id: string) { 98 | return EXT_RE.test(id) 99 | } 100 | 101 | /** 短横线命名法 */ 102 | export function kebabCase(key: string) { 103 | if (!key) 104 | return key 105 | 106 | const result = key.replace(/([A-Z])/g, ' $1').trim() 107 | return result.split(' ').join('-').toLowerCase() 108 | } 109 | 110 | /** 查找第一个不连续的数字 */ 111 | export function findFirstNonConsecutive(arr: number[]): number | null { 112 | if (arr.length < 2) 113 | return null // 如果数组长度小于2,直接返回null 114 | 115 | const result = arr.find((value, index) => index > 0 && value !== arr[index - 1] + 1) 116 | return result !== undefined ? result : null 117 | } 118 | 119 | /** 查找第一个不连续的数字之前的数字 */ 120 | export function findFirstNonConsecutiveBefore(arr: number[]): number | null { 121 | if (arr.length < 2) 122 | return null // 如果数组长度小于2,直接返回null 123 | 124 | const result = arr.find((value, index) => index > 0 && value !== arr[index - 1] + 1) 125 | return (result !== undefined && result !== null) ? arr[arr.indexOf(result) - 1] : null 126 | } 127 | 128 | /** 明确的 bool 型做取反,空值原样返回 */ 129 | export function toggleBoolean(value: boolean | undefined | null) { 130 | return typeof value === 'boolean' ? !value : value 131 | } 132 | 133 | /** 134 | * 解析 URL 查询字符串 135 | * @param url 待解析的 URL 字符串(必须包含 `?`) 136 | * @returns 解析后的查询参数对象 137 | */ 138 | export function parseQuerystring(url?: any) { 139 | if (!url || typeof url !== 'string') { 140 | return null 141 | } 142 | 143 | const rmExtUrl = url.replace(EXT_RE, '') 144 | const queryStr = rmExtUrl.split('?')[1] || '' 145 | // 此处表明函数入参的字符串必须包含 '?' 146 | if (!queryStr) { 147 | return null 148 | } 149 | 150 | try { 151 | return Object.entries(querystring.parse(queryStr)) 152 | .reduce((acc, [key, value]) => { 153 | acc[key] = value === null || value === 'true' ? true : value === 'false' ? false : value 154 | return acc 155 | }, {} as Record) 156 | } 157 | catch (error) { 158 | console.error('Error parsing query string:', error) 159 | return null 160 | } 161 | } 162 | 163 | export * from './lex-parse' 164 | export * from './regexp' 165 | export * from './vite' 166 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | build: 13 | name: 构建并发版 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: 检出代码 18 | uses: actions/checkout@v4 19 | 20 | - name: 获取当前和上一个标签 21 | id: get_tags 22 | run: | 23 | git fetch --prune --unshallow 24 | tags=($(git tag -l --sort=-version:refname)) 25 | current_tag=${tags[0]} 26 | previous_tag=${tags[1]} 27 | echo "previous_tag=$previous_tag" >> $GITHUB_OUTPUT 28 | echo "current_tag=$current_tag" >> $GITHUB_OUTPUT 29 | 30 | - name: 提取并分类提交消息 31 | id: extract_commit_messages 32 | run: | 33 | set -e 34 | current_tag="${{ steps.get_tags.outputs.current_tag }}" 35 | previous_tag="${{ steps.get_tags.outputs.previous_tag }}" 36 | if [ -z "$previous_tag" ]; then 37 | commit_messages=$(git log --pretty=format:"%s - by @%an (%h)" "$current_tag" | grep -E 'feat|fix|docs|perf' || true) 38 | else 39 | commit_messages=$(git log --pretty=format:"%s - by @%an (%h)" "$previous_tag".."$current_tag" | grep -E 'feat|fix|docs|perf' || true) 40 | fi 41 | 42 | # 转义 ` 字符 43 | commit_messages=$(echo "$commit_messages" | sed 's/`/\\\`/g') 44 | 45 | # feat_messages=$(echo "$commit_messages" | grep 'feat' || true) 46 | # fix_messages=$(echo "$commit_messages" | grep 'fix' || true) 47 | # docs_messages=$(echo "$commit_messages" | grep 'docs' || true) 48 | # perf_messages=$(echo "$commit_messages" | grep 'perf' || true) 49 | 50 | # feat_messages=("${feat_messages[@]//\`/\\\`}") 51 | # fix_messages=("${fix_messages[@]//\`/\\\`}") 52 | # docs_messages=("${docs_messages[@]//\`/\\\`}") 53 | # perf_messages=("${perf_messages[@]//\`/\\\`}") 54 | 55 | # echo "feat_messages=(${feat_messages[@]})" >> $GITHUB_OUTPUT 56 | # echo "fix_messages=(${fix_messages[@]})" >> $GITHUB_OUTPUT 57 | # echo "docs_messages=(${docs_messages[@]})" >> $GITHUB_OUTPUT 58 | # echo "perf_messages=(${perf_messages[@]})" >> $GITHUB_OUTPUT 59 | 60 | { 61 | echo 'feat_messages<> $GITHUB_OUTPUT 65 | { 66 | echo 'fix_messages<> $GITHUB_OUTPUT 70 | { 71 | echo 'docs_messages<> $GITHUB_OUTPUT 75 | { 76 | echo 'perf_messages<> $GITHUB_OUTPUT 80 | 81 | - name: 获取当前分支名 82 | id: get_branch_name 83 | run: | 84 | branch_name=$(git rev-parse --abbrev-ref HEAD) 85 | echo "branch_name=$branch_name" >> $GITHUB_OUTPUT 86 | 87 | - name: 发版详情 88 | id: generate_release_notes 89 | run: | 90 | # 提取提交消息分类 91 | feat_messages=("${{ steps.extract_commit_messages.outputs.feat_messages }}") 92 | fix_messages=("${{ steps.extract_commit_messages.outputs.fix_messages }}") 93 | docs_messages=("${{ steps.extract_commit_messages.outputs.docs_messages }}") 94 | perf_messages=("${{ steps.extract_commit_messages.outputs.perf_messages }}") 95 | 96 | release_notes="" 97 | 98 | if [[ -n "$feat_messages" ]]; then 99 | release_notes="$release_notes\n### 🚀 Features 新功能: \n" 100 | while IFS= read -r message; do 101 | release_notes="$release_notes\n- $message" 102 | done <<< "$feat_messages" 103 | fi 104 | 105 | if [[ -n "$fix_messages" ]]; then 106 | release_notes="$release_notes\n### 🩹 Fixes 缺陷修复: \n" 107 | while IFS= read -r message; do 108 | release_notes="$release_notes\n- $message" 109 | done <<< "$fix_messages" 110 | fi 111 | 112 | if [[ -n "$docs_messages" ]]; then 113 | release_notes="$release_notes\n### 📖 Documentation 文档: \n" 114 | while IFS= read -r message; do 115 | release_notes="$release_notes\n- $message" 116 | done <<< "$docs_messages" 117 | fi 118 | 119 | if [[ -n "$perf_messages" ]]; then 120 | release_notes="$release_notes\n### 🔥 Performance 性能优化: \n" 121 | while IFS= read -r message; do 122 | release_notes="$release_notes\n- $message" 123 | done <<< "$perf_messages" 124 | fi 125 | 126 | # 转义 ` 字符 127 | release_notes=$(echo "$release_notes" | sed 's/`/\\\`/g') 128 | echo "release_notes=$release_notes" >> $GITHUB_OUTPUT 129 | 130 | - name: 写入生成的发布说明到 changelog.md 131 | run: | 132 | echo -e "${{ steps.generate_release_notes.outputs.release_notes }}" > changelog.md 133 | cat changelog.md 134 | 135 | - name: 引用 changelog.md 创建发版 136 | id: release_tag 137 | uses: ncipollo/release-action@v1.14.0 138 | with: 139 | bodyFile: changelog.md 140 | -------------------------------------------------------------------------------- /src/utils/query-string/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Options, ParsedQuery, ParserForArrayFormat, SetRequired } from './types' 2 | import splitOnFirst from '../split-on-first' 3 | import { decode, isNil } from './utils' 4 | 5 | /** 6 | * 7 | * @link https://github.com/sindresorhus/query-string/blob/dc13d74d1350f8a6504b50193b8d3b60078dffaf/base.js#L357 8 | */ 9 | export function parse(query: string, options: Options = {}): ParsedQuery { 10 | options = { 11 | decode: true, 12 | arrayFormat: 'none', 13 | arrayFormatSeparator: ',', 14 | types: Object.create(null), 15 | ...options, 16 | } 17 | 18 | if (!validateArrayFormatSeparator(options.arrayFormatSeparator)) { 19 | throw new TypeError('arrayFormatSeparator must be single character string') 20 | } 21 | const formatter = parserForArrayFormat(options as SetRequired) 22 | 23 | // Create an object with no prototype 24 | const returnValue = Object.create(null) 25 | 26 | if (typeof query !== 'string') { 27 | return returnValue 28 | } 29 | 30 | // remove the special leading characters 31 | query = query.trim().replace(/^[?#&]/, '') 32 | 33 | if (!query) { 34 | return returnValue 35 | } 36 | 37 | for (const pair of query.split('&')) { 38 | if (pair === '') { 39 | continue 40 | } 41 | 42 | const parameter = options.decode ? pair.replaceAll('+', ' ') : pair 43 | 44 | let [key, value] = splitOnFirst(parameter, '=') 45 | 46 | key ??= parameter 47 | 48 | value = isNil(value) 49 | ? null 50 | : decode(value, options) 51 | 52 | formatter(decode(key, options), value, returnValue) 53 | } 54 | 55 | return returnValue 56 | } 57 | 58 | function validateArrayFormatSeparator(value: unknown): value is string & { length: 1 } { 59 | return typeof value === 'string' && value.length === 1 60 | } 61 | 62 | function parserForArrayFormat(options: SetRequired): ParserForArrayFormat { 63 | let result: RegExpExecArray | null = null 64 | 65 | function canBeArray(value: unknown) { 66 | return typeof value === 'string' && value.includes(options.arrayFormatSeparator) 67 | } 68 | function canBeEncodedArray(value: unknown) { 69 | return typeof value === 'string' && !canBeArray(value) && decode(value, options).includes(options.arrayFormatSeparator) 70 | } 71 | function tryBeArray(value: unknown) { 72 | return canBeArray(value) || canBeEncodedArray(value) 73 | } 74 | 75 | switch (options.arrayFormat) { 76 | case 'index': { 77 | return (key, value, accumulator) => { 78 | result = /\[(\d*)\]$/.exec(key) 79 | 80 | key = key.replace(/\[\d*\]$/, '') 81 | 82 | if (!result) { 83 | accumulator[key] = value 84 | return 85 | } 86 | 87 | if (accumulator[key] === undefined) { 88 | accumulator[key] = {} 89 | } 90 | accumulator[key][result[1]] = value 91 | } 92 | } 93 | 94 | case 'bracket': { 95 | return (key, value, accumulator) => { 96 | result = /(\[\])$/.exec(key) 97 | key = key.replace(/\[\]$/, '') 98 | 99 | if (!result) { 100 | accumulator[key] = value 101 | return 102 | } 103 | 104 | if (accumulator[key] === undefined) { 105 | accumulator[key] = [value] 106 | return 107 | } 108 | 109 | accumulator[key] = [...accumulator[key], value] 110 | } 111 | } 112 | 113 | case 'colon-list-separator': { 114 | return (key, value, accumulator) => { 115 | result = /(:list)$/.exec(key) 116 | key = key.replace(/:list$/, '') 117 | 118 | if (!result) { 119 | accumulator[key] = value 120 | return 121 | } 122 | 123 | if (accumulator[key] === undefined) { 124 | accumulator[key] = [value] 125 | return 126 | } 127 | 128 | accumulator[key] = [...accumulator[key], value] 129 | } 130 | } 131 | 132 | case 'comma': 133 | case 'separator': { 134 | return (key, value, accumulator) => { 135 | value = isNil(value) ? value : canBeEncodedArray(value) ? decode(value, options) : value 136 | 137 | const newValue = isNil(value) 138 | ? value 139 | : tryBeArray(value) 140 | ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) 141 | : decode(value, options) 142 | 143 | accumulator[key] = newValue 144 | } 145 | } 146 | 147 | case 'bracket-separator': { 148 | return (key, value, accumulator) => { 149 | const isArray = /\[\]$/.test(key) 150 | key = key.replace(/\[\]$/, '') 151 | 152 | if (!isArray) { 153 | accumulator[key] = value ? decode(value, options) : value 154 | return 155 | } 156 | 157 | const arrayValue = isNil(value) 158 | ? [] 159 | : decode(value, options).split(options.arrayFormatSeparator) 160 | 161 | if (accumulator[key] === undefined) { 162 | accumulator[key] = arrayValue 163 | return 164 | } 165 | 166 | accumulator[key] = [...accumulator[key], ...arrayValue] 167 | } 168 | } 169 | 170 | default: { 171 | return (key, value, accumulator) => { 172 | if (accumulator[key] === undefined) { 173 | accumulator[key] = value 174 | return 175 | } 176 | 177 | accumulator[key] = [...[accumulator[key]].flat(), value] 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/utils/segment-iterator/index.ts: -------------------------------------------------------------------------------- 1 | import type { SegmentCallback, SegmentInfo, SegmentIterator, SegmentIteratorOptions, SegmentPosition } from './type' 2 | 3 | /** 4 | * 获取下一个分段 5 | */ 6 | function getNextSegment(text: string, options: Required): { segment: string, position: SegmentPosition } { 7 | const { maxLength, breakChars, searchRange, includeSeparator } = options 8 | 9 | if (text.length <= maxLength) { 10 | return { 11 | segment: text, 12 | position: { type: 'full', index: text.length }, 13 | } 14 | } 15 | 16 | const searchStart = Math.max(0, maxLength - searchRange) 17 | const searchEnd = Math.min(text.length, maxLength + searchRange) 18 | const searchSegment = text.substring(searchStart, searchEnd) 19 | 20 | let breakIndex = -1 21 | let breakChar = '' 22 | 23 | // 从后往前查找断点字符 24 | for (let i = searchSegment.length - 1; i >= 0; i--) { 25 | if (breakChars.includes(searchSegment[i])) { 26 | breakIndex = searchStart + i 27 | breakChar = searchSegment[i] 28 | break 29 | } 30 | } 31 | 32 | let segmentEnd: number 33 | let positionType: SegmentPosition['type'] 34 | 35 | if (breakIndex !== -1) { 36 | segmentEnd = includeSeparator ? breakIndex + 1 : breakIndex 37 | positionType = 'separator' 38 | } 39 | else { 40 | segmentEnd = maxLength 41 | positionType = 'hard' 42 | } 43 | 44 | return { 45 | segment: text.substring(0, segmentEnd), 46 | position: { 47 | type: positionType, 48 | index: segmentEnd, 49 | char: breakChar, 50 | }, 51 | } 52 | } 53 | 54 | /** 55 | * 计算总段数 56 | */ 57 | function calculateTotalSegments(text: string, options: Required): number { 58 | if (!text) 59 | return 0 60 | 61 | let count = 0 62 | let remaining = text 63 | 64 | while (remaining.length > 0) { 65 | count++ 66 | const segmentInfo = getNextSegment(remaining, options) 67 | remaining = remaining.substring(segmentInfo.segment.length) 68 | } 69 | 70 | return count 71 | } 72 | 73 | /** 74 | * 创建分段迭代器 75 | */ 76 | export function createSegmentIterator(text: string, options: SegmentIteratorOptions = {}): SegmentIterator { 77 | const defaultOptions: Required = { 78 | maxLength: 100, 79 | breakChars: ['/', '\\', '.', '-', '_', ' ', '?', '&', '='], 80 | searchRange: 30, 81 | includeSeparator: false, 82 | } 83 | 84 | const mergedOptions: Required = { ...defaultOptions, ...options } 85 | 86 | let remaining = text || '' 87 | let index = 0 88 | const totalSegments = calculateTotalSegments(text, mergedOptions) 89 | 90 | const iterator: SegmentIterator = { 91 | *[Symbol.iterator](): Generator { 92 | while (remaining.length > 0) { 93 | const segmentInfo = getNextSegment(remaining, mergedOptions) 94 | const segment = segmentInfo.segment 95 | const isLast = remaining.length === segment.length 96 | 97 | const result: SegmentInfo = { 98 | segment, 99 | index, 100 | total: totalSegments, 101 | isFirst: index === 0, 102 | isLast, 103 | position: segmentInfo.position, 104 | } 105 | 106 | remaining = remaining.substring(segment.length) 107 | index++ 108 | 109 | yield result 110 | } 111 | }, 112 | 113 | toArray(): SegmentInfo[] { 114 | return Array.from(this) 115 | }, 116 | 117 | map(callback: (info: SegmentInfo, index: number, total: number) => T): T[] { 118 | const results: T[] = [] 119 | let currentIndex = 0 120 | 121 | for (const segmentInfo of this) { 122 | const result = callback(segmentInfo, currentIndex, totalSegments) 123 | results.push(result) 124 | currentIndex++ 125 | } 126 | 127 | return results 128 | }, 129 | 130 | join(separator: string = '', callback?: (info: SegmentInfo, index: number,) => string): string { 131 | const segments: string[] = [] 132 | let currentIndex = 0 133 | 134 | for (const segmentInfo of this) { 135 | const segmentText = callback 136 | ? callback(segmentInfo, currentIndex) 137 | : segmentInfo.segment 138 | segments.push(segmentText) 139 | currentIndex++ 140 | } 141 | 142 | return segments.join(separator) 143 | }, 144 | } 145 | 146 | return iterator 147 | } 148 | 149 | /** 150 | * 分段处理器基础封装 151 | */ 152 | export function processSegments( 153 | text: string, 154 | options: SegmentIteratorOptions = {}, 155 | callback?: SegmentCallback, 156 | joinSeparator: string = '
', 157 | ): string { 158 | const iterator = createSegmentIterator(text, options) 159 | 160 | if (callback) { 161 | return iterator.join(joinSeparator, info => callback(info, info.index, info.total)) 162 | } 163 | 164 | return iterator.join(joinSeparator) 165 | } 166 | 167 | // /** 168 | // * 预置特定类型的分段器 169 | // */ 170 | // // eslint-disable-next-line ts/no-namespace, unused-imports/no-unused-vars 171 | // namespace Segmenters { 172 | // export function createPathSegmenter(maxLength: number = 100): (text: string) => SegmentIterator { 173 | // return (text: string) => createSegmentIterator(text, { 174 | // maxLength, 175 | // breakChars: ['/', '\\'], 176 | // includeSeparator: true, 177 | // }) 178 | // } 179 | 180 | // export function createTextSegmenter(maxLength: number = 80): (text: string) => SegmentIterator { 181 | // return (text: string) => createSegmentIterator(text, { 182 | // maxLength, 183 | // breakChars: [' ', ',', '.', ';', ':', '!', '?'], 184 | // includeSeparator: false, 185 | // }) 186 | // } 187 | 188 | // export function createCodeSegmenter(maxLength: number = 80): (text: string) => SegmentIterator { 189 | // return (text: string) => createSegmentIterator(text, { 190 | // maxLength, 191 | // breakChars: ['.', ',', ';', '(', ')', '{', '}', '[', ']', '=', '+', '-', '*', '/'], 192 | // includeSeparator: false, 193 | // }) 194 | // } 195 | // } 196 | -------------------------------------------------------------------------------- /examples/common-error-http-sdk/lixin/common/error/v1/error.enum.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts. DO NOT EDIT. 2 | // Code generated by protoc-gen-ts. DO NOT EDIT. 3 | // Code generated by protoc-gen-ts. DO NOT EDIT. 4 | 5 | // buf:lint:ignore ENUM_VALUE_PREFIX 6 | // Grpc 错误码 7 | export enum ErrorReasonStrValue { 8 | // 缺省错误 9 | ERROR_REASON_UNSPECIFIED = 'ERROR_REASON_UNSPECIFIED', 10 | // The operation was cancelled, typically by the caller. 11 | // 12 | // HTTP Mapping: 499 Client Closed Request 13 | CANCELLED = 'CANCELLED', 14 | // Unknown error. For example, this error may be returned when 15 | // a `Status` value received from another address space belongs to 16 | // an error space that is not known in this address space. Also 17 | // errors raised by APIs that do not return enough error information 18 | // may be converted to this error. 19 | // 20 | // HTTP Mapping: 500 Internal Server Error 21 | UNKNOWN = 'UNKNOWN', 22 | // The client specified an invalid argument. Note that this differs 23 | // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments 24 | // that are problematic regardless of the state of the system 25 | // (e.g., a malformed file name). 26 | // 27 | // HTTP Mapping: 400 Bad Request 28 | INVALID_ARGUMENT = 'INVALID_ARGUMENT', 29 | // The deadline expired before the operation could complete. For operations 30 | // that change the state of the system, this error may be returned 31 | // even if the operation has completed successfully. For example, a 32 | // successful response from a server could have been delayed long 33 | // enough for the deadline to expire. 34 | // 35 | // HTTP Mapping: 504 Gateway Timeout 36 | DEADLINE_EXCEEDED = 'DEADLINE_EXCEEDED', 37 | // Some requested entity (e.g., file or directory) was not found. 38 | // 39 | // Note to server developers: if a request is denied for an entire class 40 | // of users, such as gradual feature rollout or undocumented whitelist, 41 | // `NOT_FOUND` may be used. If a request is denied for some users within 42 | // a class of users, such as user-based access control, `PERMISSION_DENIED` 43 | // must be used. 44 | // 45 | // HTTP Mapping: 404 Not Found 46 | NOT_FOUND = 'NOT_FOUND', 47 | // The entity that a client attempted to create (e.g., file or directory) 48 | // already exists. 49 | // 50 | // HTTP Mapping: 409 Conflict 51 | ALREADY_EXISTS = 'ALREADY_EXISTS', 52 | // The caller does not have permission to execute the specified 53 | // operation. `PERMISSION_DENIED` must not be used for rejections 54 | // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` 55 | // instead for those errors). `PERMISSION_DENIED` must not be 56 | // used if the caller can not be identified (use `UNAUTHENTICATED` 57 | // instead for those errors). This error code does not imply the 58 | // request is valid or the requested entity exists or satisfies 59 | // other pre-conditions. 60 | // 61 | // HTTP Mapping: 403 Forbidden 62 | PERMISSION_DENIED = 'PERMISSION_DENIED', 63 | // The request does not have valid authentication credentials for the 64 | // operation. 65 | // 66 | // HTTP Mapping: 401 Unauthorized 67 | UNAUTHENTICATED = 'UNAUTHENTICATED', 68 | // Some resource has been exhausted, perhaps a per-user quota, or 69 | // perhaps the entire file system is out of space. 70 | // 71 | // HTTP Mapping: 429 Too Many Requests 72 | RESOURCE_EXHAUSTED = 'RESOURCE_EXHAUSTED', 73 | // The operation was rejected because the system is not in a state 74 | // required for the operation's execution. For example, the directory 75 | // to be deleted is non-empty, an rmdir operation is applied to 76 | // a non-directory, etc. 77 | // 78 | // Service implementors can use the following guidelines to decide 79 | // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: 80 | // (a) Use `UNAVAILABLE` if the client can retry just the failing call. 81 | // (b) Use `ABORTED` if the client should retry at a higher level 82 | // (e.g., when a client-specified test-and-set fails, indicating the 83 | // client should restart a read-modify-write sequence). 84 | // (c) Use `FAILED_PRECONDITION` if the client should not retry until 85 | // the system state has been explicitly fixed. E.g., if an "rmdir" 86 | // fails because the directory is non-empty, `FAILED_PRECONDITION` 87 | // should be returned since the client should not retry unless 88 | // the files are deleted from the directory. 89 | // 90 | // HTTP Mapping: 400 Bad Request 91 | FAILED_PRECONDITION = 'FAILED_PRECONDITION', 92 | // The operation was aborted, typically due to a concurrency issue such as 93 | // a sequencer check failure or transaction abort. 94 | // 95 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 96 | // `ABORTED`, and `UNAVAILABLE`. 97 | // 98 | // HTTP Mapping: 409 Conflict 99 | ABORTED = 'ABORTED', 100 | // The operation was attempted past the valid range. E.g., seeking or 101 | // reading past end-of-file. 102 | // 103 | // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may 104 | // be fixed if the system state changes. For example, a 32-bit file 105 | // system will generate `INVALID_ARGUMENT` if asked to read at an 106 | // offset that is not in the range [0,2^32-1], but it will generate 107 | // `OUT_OF_RANGE` if asked to read from an offset past the current 108 | // file size. 109 | // 110 | // There is a fair bit of overlap between `FAILED_PRECONDITION` and 111 | // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific 112 | // error) when it applies so that callers who are iterating through 113 | // a space can easily look for an `OUT_OF_RANGE` error to detect when 114 | // they are done. 115 | // 116 | // HTTP Mapping: 400 Bad Request 117 | OUT_OF_RANGE = 'OUT_OF_RANGE', 118 | // The operation is not implemented or is not supported/enabled in this 119 | // service. 120 | // 121 | // HTTP Mapping: 501 Not Implemented 122 | UNIMPLEMENTED = 'UNIMPLEMENTED', 123 | // Internal errors. This means that some invariants expected by the 124 | // underlying system have been broken. This error code is reserved 125 | // for serious errors. 126 | // 127 | // HTTP Mapping: 500 Internal Server Error 128 | INTERNAL = 'INTERNAL', 129 | // The service is currently unavailable. This is most likely a 130 | // transient condition, which can be corrected by retrying with 131 | // a backoff. Note that it is not always safe to retry 132 | // non-idempotent operations. 133 | // 134 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 135 | // `ABORTED`, and `UNAVAILABLE`. 136 | // 137 | // HTTP Mapping: 503 Service Unavailable 138 | UNAVAILABLE = 'UNAVAILABLE', 139 | // Unrecoverable data loss or corruption. 140 | // 141 | // HTTP Mapping: 500 Internal Server Error 142 | DATA_LOSS = 'DATA_LOSS', 143 | } 144 | -------------------------------------------------------------------------------- /examples/common-error-http-sdk/lixin/common/error/v1/error.ts: -------------------------------------------------------------------------------- 1 | export const protobufPackage = 'lixin.common.error.v1' 2 | 3 | /** 4 | * buf:lint:ignore ENUM_VALUE_PREFIX 5 | * Grpc 错误码 6 | */ 7 | export enum ErrorReason { 8 | /** ERROR_REASON_UNSPECIFIED - 缺省错误 */ 9 | ERROR_REASON_UNSPECIFIED = 0, 10 | /** 11 | * CANCELLED - The operation was cancelled, typically by the caller. 12 | * 13 | * HTTP Mapping: 499 Client Closed Request 14 | */ 15 | CANCELLED = 1, 16 | /** 17 | * UNKNOWN - Unknown error. For example, this error may be returned when 18 | * a `Status` value received from another address space belongs to 19 | * an error space that is not known in this address space. Also 20 | * errors raised by APIs that do not return enough error information 21 | * may be converted to this error. 22 | * 23 | * HTTP Mapping: 500 Internal Server Error 24 | */ 25 | UNKNOWN = 2, 26 | /** 27 | * INVALID_ARGUMENT - The client specified an invalid argument. Note that this differs 28 | * from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments 29 | * that are problematic regardless of the state of the system 30 | * (e.g., a malformed file name). 31 | * 32 | * HTTP Mapping: 400 Bad Request 33 | */ 34 | INVALID_ARGUMENT = 3, 35 | /** 36 | * DEADLINE_EXCEEDED - The deadline expired before the operation could complete. For operations 37 | * that change the state of the system, this error may be returned 38 | * even if the operation has completed successfully. For example, a 39 | * successful response from a server could have been delayed long 40 | * enough for the deadline to expire. 41 | * 42 | * HTTP Mapping: 504 Gateway Timeout 43 | */ 44 | DEADLINE_EXCEEDED = 4, 45 | /** 46 | * NOT_FOUND - Some requested entity (e.g., file or directory) was not found. 47 | * 48 | * Note to server developers: if a request is denied for an entire class 49 | * of users, such as gradual feature rollout or undocumented whitelist, 50 | * `NOT_FOUND` may be used. If a request is denied for some users within 51 | * a class of users, such as user-based access control, `PERMISSION_DENIED` 52 | * must be used. 53 | * 54 | * HTTP Mapping: 404 Not Found 55 | */ 56 | NOT_FOUND = 5, 57 | /** 58 | * ALREADY_EXISTS - The entity that a client attempted to create (e.g., file or directory) 59 | * already exists. 60 | * 61 | * HTTP Mapping: 409 Conflict 62 | */ 63 | ALREADY_EXISTS = 6, 64 | /** 65 | * PERMISSION_DENIED - The caller does not have permission to execute the specified 66 | * operation. `PERMISSION_DENIED` must not be used for rejections 67 | * caused by exhausting some resource (use `RESOURCE_EXHAUSTED` 68 | * instead for those errors). `PERMISSION_DENIED` must not be 69 | * used if the caller can not be identified (use `UNAUTHENTICATED` 70 | * instead for those errors). This error code does not imply the 71 | * request is valid or the requested entity exists or satisfies 72 | * other pre-conditions. 73 | * 74 | * HTTP Mapping: 403 Forbidden 75 | */ 76 | PERMISSION_DENIED = 7, 77 | /** 78 | * UNAUTHENTICATED - The request does not have valid authentication credentials for the 79 | * operation. 80 | * 81 | * HTTP Mapping: 401 Unauthorized 82 | */ 83 | UNAUTHENTICATED = 16, 84 | /** 85 | * RESOURCE_EXHAUSTED - Some resource has been exhausted, perhaps a per-user quota, or 86 | * perhaps the entire file system is out of space. 87 | * 88 | * HTTP Mapping: 429 Too Many Requests 89 | */ 90 | RESOURCE_EXHAUSTED = 8, 91 | /** 92 | * FAILED_PRECONDITION - The operation was rejected because the system is not in a state 93 | * required for the operation's execution. For example, the directory 94 | * to be deleted is non-empty, an rmdir operation is applied to 95 | * a non-directory, etc. 96 | * 97 | * Service implementors can use the following guidelines to decide 98 | * between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: 99 | * (a) Use `UNAVAILABLE` if the client can retry just the failing call. 100 | * (b) Use `ABORTED` if the client should retry at a higher level 101 | * (e.g., when a client-specified test-and-set fails, indicating the 102 | * client should restart a read-modify-write sequence). 103 | * (c) Use `FAILED_PRECONDITION` if the client should not retry until 104 | * the system state has been explicitly fixed. E.g., if an "rmdir" 105 | * fails because the directory is non-empty, `FAILED_PRECONDITION` 106 | * should be returned since the client should not retry unless 107 | * the files are deleted from the directory. 108 | * 109 | * HTTP Mapping: 400 Bad Request 110 | */ 111 | FAILED_PRECONDITION = 9, 112 | /** 113 | * ABORTED - The operation was aborted, typically due to a concurrency issue such as 114 | * a sequencer check failure or transaction abort. 115 | * 116 | * See the guidelines above for deciding between `FAILED_PRECONDITION`, 117 | * `ABORTED`, and `UNAVAILABLE`. 118 | * 119 | * HTTP Mapping: 409 Conflict 120 | */ 121 | ABORTED = 10, 122 | /** 123 | * OUT_OF_RANGE - The operation was attempted past the valid range. E.g., seeking or 124 | * reading past end-of-file. 125 | * 126 | * Unlike `INVALID_ARGUMENT`, this error indicates a problem that may 127 | * be fixed if the system state changes. For example, a 32-bit file 128 | * system will generate `INVALID_ARGUMENT` if asked to read at an 129 | * offset that is not in the range [0,2^32-1], but it will generate 130 | * `OUT_OF_RANGE` if asked to read from an offset past the current 131 | * file size. 132 | * 133 | * There is a fair bit of overlap between `FAILED_PRECONDITION` and 134 | * `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific 135 | * error) when it applies so that callers who are iterating through 136 | * a space can easily look for an `OUT_OF_RANGE` error to detect when 137 | * they are done. 138 | * 139 | * HTTP Mapping: 400 Bad Request 140 | */ 141 | OUT_OF_RANGE = 11, 142 | /** 143 | * UNIMPLEMENTED - The operation is not implemented or is not supported/enabled in this 144 | * service. 145 | * 146 | * HTTP Mapping: 501 Not Implemented 147 | */ 148 | UNIMPLEMENTED = 12, 149 | /** 150 | * INTERNAL - Internal errors. This means that some invariants expected by the 151 | * underlying system have been broken. This error code is reserved 152 | * for serious errors. 153 | * 154 | * HTTP Mapping: 500 Internal Server Error 155 | */ 156 | INTERNAL = 13, 157 | /** 158 | * UNAVAILABLE - The service is currently unavailable. This is most likely a 159 | * transient condition, which can be corrected by retrying with 160 | * a backoff. Note that it is not always safe to retry 161 | * non-idempotent operations. 162 | * 163 | * See the guidelines above for deciding between `FAILED_PRECONDITION`, 164 | * `ABORTED`, and `UNAVAILABLE`. 165 | * 166 | * HTTP Mapping: 503 Service Unavailable 167 | */ 168 | UNAVAILABLE = 14, 169 | /** 170 | * DATA_LOSS - Unrecoverable data loss or corruption. 171 | * 172 | * HTTP Mapping: 500 Internal Server Error 173 | */ 174 | DATA_LOSS = 15, 175 | UNRECOGNIZED = -1, 176 | } 177 | -------------------------------------------------------------------------------- /src/utils/visualizer/transform.ts: -------------------------------------------------------------------------------- 1 | import type { ManualChunkMeta } from '../../type' 2 | import type { GraphRestrictArea, ViteNode, ViteNodeLink, ViteRestrictNode } from './type' 3 | import path from 'node:path' 4 | import { parseQuerystring } from '..' 5 | import { parseVirtualPath } from '../uniapp' 6 | import { createRestrictAreaSearcher } from './helper' 7 | 8 | /** 9 | * 从模块 ID 中解析出干净的文件名和扩展名。 10 | * @param id - Rollup 模块 ID 11 | */ 12 | function parseModuleId(id: string): { fileName: string, ext: string, name: string } { 13 | // 移除 Vite/Rollup 可能添加的前缀 (如 \0) 14 | const cleanedId = id.replace(/^\0/, '') 15 | const ext = path.extname(cleanedId) 16 | const fileName = path.basename(cleanedId) 17 | const name = path.basename(cleanedId, ext) // 不含扩展名的文件名 18 | return { fileName, ext: ext.slice(1), name } 19 | } 20 | 21 | /** uniapp 项目的匹配模式 */ 22 | function createUniappMatcher(id: string) { 23 | const matchString = `${id}/index.vue` 24 | const getBasePath = (path: string): string => { 25 | const qIndex = path.indexOf('?') 26 | return qIndex !== -1 ? path.substring(0, qIndex) : path 27 | } 28 | 29 | return (targetString: string) => { 30 | const [is, result, _] = parseVirtualPath(targetString) 31 | 32 | // 如果是 uniapp 的组件虚拟路径 33 | if (is) { 34 | // result 可能是相对路径 35 | return result === matchString || matchString.endsWith(`/${result}`) 36 | } 37 | 38 | const basePath = getBasePath(targetString) 39 | if (basePath === matchString) { 40 | const parsedUrl = parseQuerystring(targetString) 41 | // 检查是否是无参数的完全匹配,或者有查询参数的 script 类型 42 | if (!parsedUrl || (parsedUrl.type === 'script' && parsedUrl.vue === true)) { 43 | return true 44 | } 45 | // ?vue&type=style&lang.css 46 | if (parsedUrl.type === 'style' && parsedUrl.vue === true && targetString.endsWith('.css')) { 47 | return 'css' 48 | } 49 | } 50 | 51 | return false 52 | } 53 | } 54 | 55 | type MergeNode = D extends object ? N & D : N 56 | type AsNode = 57 | Restrict extends true 58 | ? MergeNode 59 | : MergeNode 60 | 61 | interface TransformRes { 62 | nodes: Array> 63 | links: ViteNodeLink[] 64 | } 65 | 66 | interface TransformDataFunction { 67 | /** 68 | * 将 Rollup/Vite 的模块图数据转换为 ECharts 可用的格式 69 | * @param pluginContext - Rollup 插件的上下文对象 (`this`) 70 | * @returns 包含 nodes 和 links 的对象 71 | */ 72 | ( 73 | pluginContext: ManualChunkMeta, 74 | onSet?: (node: T) => D 75 | ): TransformRes 76 | 77 | /** 78 | * 将 Rollup/Vite 的模块图数据转换为 ECharts 可用的格式 79 | * @param pluginContext - Rollup 插件的上下文对象 (`this`) 80 | * @param restrictAreas - 受限域信息(需要含有 children 内容) 81 | * @returns 包含 nodes 和 links 的对象 82 | */ 83 | ( 84 | pluginContext: ManualChunkMeta, 85 | onSet?: (node: T) => D, 86 | restrictAreas?: GraphRestrictArea[] 87 | ): TransformRes 88 | } 89 | 90 | export const transformDataForECharts: TransformDataFunction = ( 91 | pluginContext: ManualChunkMeta, 92 | onSet?: (node: T) => D, 93 | restrictAreas?: GraphRestrictArea[], 94 | ) => { 95 | const nodeMap = new Map() 96 | const links: ViteNodeLink[] = [] 97 | 98 | // TODO: 在加入 map 时,从 restrictAreas 查找 node.id 是否在对应的受限域中 99 | const restrictAreaSearcher = createRestrictAreaSearcher(restrictAreas, createUniappMatcher) 100 | 101 | const execOnSet = (node: T): D => { 102 | if (typeof onSet === 'function') { 103 | const target = onSet(node) 104 | if (typeof target === 'object') 105 | return target 106 | } 107 | return node as D 108 | } 109 | 110 | // ================================================================= 111 | // 收集所有节点和链接 112 | // ================================================================= 113 | const moduleIds = Array.from(pluginContext.getModuleIds()) 114 | for (const id of moduleIds) { 115 | const moduleInfo = pluginContext.getModuleInfo(id) 116 | 117 | if (!moduleInfo) 118 | continue 119 | 120 | // 创建或更新当前模块的节点 (Source Node) 121 | if (!nodeMap.has(id)) { 122 | const { fileName: name, name: label } = parseModuleId(id) 123 | const [area, matcheRes] = restrictAreaSearcher(id) 124 | if (typeof matcheRes === 'string') { 125 | nodeMap.set(id, execOnSet({ 126 | id, 127 | name, 128 | label, 129 | type: 'asset', 130 | resourceType: matcheRes, 131 | area, 132 | } as T)) 133 | } 134 | else { 135 | nodeMap.set(id, execOnSet({ 136 | id, 137 | name, 138 | label, 139 | type: 'chunk', 140 | isEntry: moduleInfo.isEntry, 141 | area, 142 | } as T)) 143 | } 144 | } 145 | 146 | // 处理静态依赖 (Static Imports) 147 | for (const targetId of moduleInfo.importedIds) { 148 | // 确保目标节点也存在于 nodeMap 中 149 | if (!nodeMap.has(targetId)) { 150 | // 如果目标节点不存在,创建一个基础表示 151 | const { fileName: name, name: label } = parseModuleId(targetId) 152 | const targetModuleInfo = pluginContext.getModuleInfo(targetId) 153 | const [area, matcheRes] = restrictAreaSearcher(targetId) 154 | if (typeof matcheRes === 'string') { 155 | nodeMap.set(targetId, execOnSet({ 156 | id: targetId, 157 | name, 158 | label, 159 | type: 'asset', 160 | resourceType: matcheRes, 161 | area, 162 | } as T)) 163 | } 164 | else { 165 | nodeMap.set(targetId, execOnSet({ 166 | id: targetId, 167 | name, 168 | label, 169 | type: 'chunk', 170 | isEntry: targetModuleInfo?.isEntry ?? false, 171 | area, 172 | } as T)) 173 | } 174 | } 175 | 176 | links.push({ 177 | source: id, 178 | target: targetId, 179 | type: 'static', 180 | }) 181 | } 182 | 183 | // 处理动态依赖 (Dynamic Imports) 184 | for (const targetId of moduleInfo.dynamicallyImportedIds) { 185 | if (!nodeMap.has(targetId)) { 186 | const { fileName: name, name: label } = parseModuleId(targetId) 187 | const targetModuleInfo = pluginContext.getModuleInfo(targetId) 188 | const [area, matcheRes] = restrictAreaSearcher(targetId) 189 | if (typeof matcheRes === 'string') { 190 | nodeMap.set(targetId, execOnSet({ 191 | id: targetId, 192 | name, 193 | label, 194 | type: 'asset', 195 | resourceType: matcheRes, 196 | area, 197 | } as T)) 198 | } 199 | else { 200 | nodeMap.set(targetId, execOnSet({ 201 | id: targetId, 202 | name, 203 | label, 204 | type: 'chunk', 205 | isEntry: targetModuleInfo?.isEntry ?? false, 206 | area, 207 | } as T)) 208 | } 209 | } 210 | 211 | links.push({ 212 | source: id, 213 | target: targetId, 214 | type: 'dynamic', 215 | }) 216 | } 217 | } 218 | 219 | // ================================================================= 220 | // 计算每个节点的入度 (in-degree) 作为引用权重 221 | // ================================================================= 222 | const inDegreeMap = new Map() 223 | 224 | // 初始化所有节点的入度为 0 225 | for (const id of nodeMap.keys()) { 226 | inDegreeMap.set(id, 0) 227 | } 228 | 229 | // 遍历所有链接,为目标节点 (target) 的入度计数 230 | for (const link of links) { 231 | const currentCount = inDegreeMap.get(link.target) ?? 0 232 | inDegreeMap.set(link.target, currentCount + 1) 233 | } 234 | 235 | // 更新 nodeMap 中每个节点的 value 236 | for (const [id, node] of nodeMap.entries()) { 237 | node.value = inDegreeMap.get(id) ?? 0 238 | } 239 | 240 | return { 241 | nodes: Array.from(nodeMap.values()), 242 | links, 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/plugin/async-import-processor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | import type { Plugin } from 'vite' 3 | import type { DtsType, OutputChunk } from '../type' 4 | import fs from 'node:fs' 5 | import path from 'node:path' 6 | import process from 'node:process' 7 | import MagicString from 'magic-string' 8 | import { AsyncImports } from '../common/AsyncImports' 9 | import { logger } from '../common/Logger' 10 | import { JS_TYPES_RE, ROOT_DIR, SRC_DIR_RE } from '../constants' 11 | import { ensureDirectoryExists, getVitePathResolver, lexFunctionCalls, moduleIdProcessor, normalizePath, parseAsyncImports, resolveAssetsPath } from '../utils' 12 | 13 | /** 14 | * 负责处理`AsyncImport`函数调用的传参路径 15 | * 16 | * @description `transform`阶段处理`AsyncImport()`函数的路径传参,将别名路径转换为真实路径 17 | * @description `generateBundle`阶段处理`AsyncImport()`函数的路径传参,进一步将路径转换为生产环境的路径(hash化的路径) 18 | * 19 | * TODO: 暂时不支持app端:首先由于app端实用的是iife模式,代码内容中无法使用`import()`语法,直接会编译报错 20 | */ 21 | export function AsyncImportProcessor(options: DtsType, enableLogger: boolean): Plugin { 22 | const platform = process.env.UNI_PLATFORM 23 | /** 是否小程序 */ 24 | const isMP = platform?.startsWith('mp') 25 | /** 是否H5 */ 26 | const isH5 = platform === 'h5' 27 | /** 是否为app */ 28 | const isApp = platform === 'app' 29 | const AsyncImportsInstance = new AsyncImports() 30 | 31 | /** 生成类型定义文件 */ 32 | function generateTypeFile(paths?: string[]) { 33 | if (options === false || options.enable === false) 34 | return 35 | 36 | const typesFilePath = path.resolve(ROOT_DIR, normalizePath(options.path)) 37 | ensureDirectoryExists(typesFilePath) 38 | let cache: string[] = [] // 缓存已经生成的类型定义,防止开发阶段热更新时部分类型定义生成丢失 39 | if (fs.existsSync(typesFilePath)) { 40 | const list = lexFunctionCalls(fs.readFileSync(typesFilePath, 'utf-8'), 'import').flatMap(({ args }) => args.map(({ value }) => value.toString())) 41 | list && list.length && (cache = Array.from(new Set(list))) 42 | } 43 | const typeDefinition = generateModuleDeclaration(paths, cache) 44 | fs.writeFileSync(typesFilePath, typeDefinition) 45 | logger.info(`[async-import] ${paths === undefined ? '初始化' : '生成'}类型定义文件 ${typesFilePath.replace(`${ROOT_DIR}\\`, '')}`, !enableLogger) 46 | } 47 | generateTypeFile() // 初始化类型定义文件 48 | 49 | logger.info('[async-import] 异步导入处理器已启用', !enableLogger) 50 | 51 | return { 52 | name: 'async-import-processor', 53 | enforce: 'post', // 插件执行时机,在其他处理后执行 54 | 55 | async transform(code, id) { 56 | const asyncImports = parseAsyncImports(code) 57 | 58 | if (asyncImports.length > 0 && !isApp) { 59 | const magicString = new MagicString(code) 60 | // 生成类型定义文件 61 | const paths = asyncImports.map(item => item.args[0].value.toString()) 62 | generateTypeFile(paths) 63 | 64 | for (const { full, args } of asyncImports) { 65 | for (const { start, end, value } of args) { 66 | // 加入缓存 67 | const target = value.toString() 68 | // target 可能是一个模块的裸引用 69 | let resolveId = (await this.resolve(target, id))?.id 70 | if (resolveId) { 71 | resolveId = moduleIdProcessor(resolveId) 72 | } 73 | 74 | AsyncImportsInstance.addCache(moduleIdProcessor(id), target, resolveId) 75 | magicString.overwrite(full.start, full.start + 'AsyncImport'.length, 'import', { contentOnly: true }) 76 | } 77 | } 78 | 79 | return { 80 | code: magicString.toString(), 81 | map: magicString.generateMap({ hires: true }), 82 | } 83 | } 84 | }, 85 | renderDynamicImport(options) { 86 | const cache = AsyncImportsInstance.getCache(moduleIdProcessor(options.moduleId)) 87 | if (cache && options.targetModuleId && !isApp && !isH5) { 88 | // 如果是js文件的话去掉后缀 89 | const targetModuleId = moduleIdProcessor(options.targetModuleId).replace(JS_TYPES_RE, '') 90 | const temp = cache.map(item => ({ 91 | value: moduleIdProcessor(item.match(/^(\.\/|\.\.\/)+/) ? path.resolve(path.dirname(options.moduleId), item) : getVitePathResolver()(item).replace(SRC_DIR_RE, 'src/')), 92 | realPath: AsyncImportsInstance.getRealPath(item)?.[0], 93 | })) 94 | 95 | if (temp.some(item => moduleIdProcessor(item.realPath ?? item.value).replace(JS_TYPES_RE, '') === targetModuleId)) { 96 | return { 97 | left: 'AsyncImport(', 98 | right: ')', 99 | } 100 | } 101 | } 102 | }, 103 | generateBundle({ format }, bundle) { 104 | // 小程序端为cjs,app端为iife 105 | if (!['es', 'cjs', 'iife'].includes(format) || isApp) 106 | return 107 | 108 | // 页面被当作组件引入了,这是允许的,但是表现不一样,此处缓存记录 109 | const pageComponents: OutputChunk[] = [] 110 | 111 | const hashFileMap = Object.entries(bundle).reduce((acc, [file, chunk]) => { 112 | if (chunk.type === 'chunk') { 113 | let moduleId = chunk.facadeModuleId ?? undefined 114 | 115 | if (moduleId?.startsWith('uniPage://') || moduleId?.startsWith('uniComponent://')) { 116 | const moduleIds = chunk.moduleIds?.filter(id => id !== moduleId).map(id => moduleIdProcessor(id)) ?? [] 117 | if (moduleIds.length >= 1 && moduleIds.length < chunk.moduleIds.length) { 118 | moduleId = moduleIds.at(-1) 119 | } 120 | else if (!moduleIds.length && chunk.fileName) { // 处理页面被当作组件引入的情况 121 | pageComponents.push(chunk) 122 | return acc 123 | } 124 | } 125 | 126 | if (moduleId) { 127 | acc[moduleIdProcessor(moduleId)] = chunk.fileName 128 | } 129 | else { 130 | // 处理其他的文件的hash化路径映射情况 131 | const temp = chunk.moduleIds?.filter(id => !id.startsWith('\x00')) ?? [] 132 | temp.forEach((id) => { 133 | acc[moduleIdProcessor(id)] = chunk.fileName 134 | }) 135 | } 136 | } 137 | 138 | return acc 139 | }, {} as Record) 140 | 141 | if (pageComponents.length) { 142 | const chunks = Object.values(bundle) 143 | for (let index = 0; index < chunks.length; index++) { 144 | const chunk = chunks[index] 145 | if (chunk.type === 'chunk') { 146 | const targetKey = Object.keys(hashFileMap).find((key) => { 147 | const value = hashFileMap[key] 148 | return typeof value === 'string' ? chunk.imports.includes(value) : value.some((item: string) => chunk.imports.includes(item)) 149 | }) 150 | if (targetKey) { 151 | const old = typeof hashFileMap[targetKey] === 'string' ? [hashFileMap[targetKey]] : hashFileMap[targetKey] || [] 152 | hashFileMap[targetKey] = [...old, chunk.fileName] 153 | } 154 | } 155 | } 156 | } 157 | 158 | for (const file in bundle) { 159 | const chunk = bundle[file] 160 | if (chunk.type === 'chunk' && chunk.code.includes('AsyncImport')) { 161 | const code = chunk.code 162 | const asyncImports = parseAsyncImports(code) 163 | 164 | if (asyncImports.length > 0) { 165 | const magicString = new MagicString(code) 166 | 167 | asyncImports.forEach(({ full, args }) => { 168 | args.forEach(({ start, end, value }) => { 169 | const url = value.toString() 170 | 171 | // 去除相对路径的前缀,例如`./`、`../`、`../../`等正确的相对路径的写法,`.../`是不正确的 172 | if ( 173 | isMP 174 | ? Object.values(hashFileMap).flat().includes(normalizePath(path.posix.join(path.dirname(chunk.fileName), url))) 175 | : Object.values(hashFileMap).flat().map(resolveAssetsPath).includes(url) 176 | ) { 177 | magicString.overwrite(full.start, full.start + 'AsyncImport'.length, isMP ? 'require.async' : 'import', { contentOnly: true }) 178 | } 179 | }) 180 | }) 181 | // 遍历完毕之后更新chunk的code 182 | chunk.code = magicString.toString() 183 | } 184 | } 185 | } 186 | }, 187 | } 188 | } 189 | 190 | export default AsyncImportProcessor 191 | 192 | /** 193 | * 生成类型定义 194 | */ 195 | function generateModuleDeclaration(paths?: string[], cache?: string[]): string { 196 | // 将路径组合成 ModuleMap 中的键 197 | const moduleMapEntries = Array.from(new Set([...(cache || []), ...(paths || [])])) 198 | ?.map((p) => { 199 | return ` '${p}': typeof import('${p}')` 200 | }) 201 | .join('\n') 202 | 203 | // 返回类型定义 204 | return `/* eslint-disable */ 205 | /* prettier-ignore */ 206 | // @ts-nocheck 207 | // Generated by @uni-ku/bundle-optimizer 208 | export {} 209 | 210 | interface ModuleMap { 211 | ${moduleMapEntries 212 | ? `${moduleMapEntries} 213 | [path: string]: any` 214 | : ' [path: string]: any' 215 | } 216 | } 217 | 218 | declare global { 219 | function AsyncImport(arg: T): Promise 220 | } 221 | ` 222 | } 223 | -------------------------------------------------------------------------------- /src/plugin/async-component-processor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | import type { Plugin } from 'vite' 3 | import type { TemplateDescriptor } from '../common/AsyncComponents' 4 | import type { DtsType } from '../type' 5 | import type { ArgumentLocation } from '../utils' 6 | import fs from 'node:fs' 7 | import path from 'node:path' 8 | import process from 'node:process' 9 | import { normalizeMiniProgramFilename, removeExt } from '@dcloudio/uni-cli-shared' 10 | import MagicString from 'magic-string' 11 | import { AsyncComponents } from '../common/AsyncComponents' 12 | import { logger } from '../common/Logger' 13 | import { ROOT_DIR } from '../constants' 14 | import { calculateRelativePath, ensureDirectoryExists, findFirstNonConsecutiveBefore, getVitePathResolver, kebabCase, lexDefaultImportWithQuery, lexFunctionCalls, normalizePath } from '../utils' 15 | 16 | export const AsyncComponentsInstance = new AsyncComponents() 17 | 18 | /** 19 | * 处理 `import xxx from "*.vue?async"` 形式的调用 20 | * @description `transform`阶段处理识别以上形式的导入语句,做相关的缓存处理;并将`?async`查询参数去除,避免后续编译处理识别不来该语句 21 | * @description `generateBundle`阶段处理生成相关页面的 page-json 文件,注入`componentPlaceholder`配置 22 | */ 23 | export function AsyncComponentProcessor(options: DtsType, enableLogger: boolean): Plugin { 24 | const inputDir = process.env.UNI_INPUT_DIR 25 | const platform = process.env.UNI_PLATFORM 26 | 27 | const isMP = platform?.startsWith('mp-') 28 | 29 | /** 生成类型定义文件 */ 30 | function generateTypeFile(parseResult?: ReturnType) { 31 | if (options === false || options.enable === false) 32 | return 33 | 34 | const typesFilePath = path.resolve(ROOT_DIR, normalizePath(options.path)) 35 | ensureDirectoryExists(typesFilePath) 36 | let cache: string[] = [] // 缓存已经生成的类型定义,防止开发阶段热更新时部分类型定义生成丢失 37 | if (fs.existsSync(typesFilePath)) { 38 | const list = lexFunctionCalls(fs.readFileSync(typesFilePath, 'utf-8'), 'import').flatMap(({ args }) => args.map(({ value }) => value.toString())) 39 | list && list.length && (cache = Array.from(new Set(list))) 40 | } 41 | const typeDefinition = generateModuleDeclaration(parseResult, cache) 42 | fs.writeFileSync(typesFilePath, typeDefinition) 43 | logger.info(`[async-component] ${parseResult === undefined ? '初始化' : '生成'}类型定义文件 ${typesFilePath.replace(`${ROOT_DIR}\\`, '')}`, !enableLogger) 44 | } 45 | generateTypeFile() // 初始化类型定义文件 46 | 47 | logger.info('[async-component] 异步组件处理器已启用', !enableLogger) 48 | 49 | return { 50 | name: 'async-component-processor', 51 | async transform(source, importer) { 52 | // 热更新时,由于含有 async 查询参数的导入语句会删除查询部分(为的是避免后续编译处理识别不来该语句) 53 | // 所以热更新代码时,已经被处理过的代码再次处理时,原本应该被处理的相关查询参数代码已经被删除了,将不会再处理该代码文件 54 | // TODO: 后续需要针对以上问题进行优化((好像解决了?) 55 | const parseResult = lexDefaultImportWithQuery(source).filter(({ modulePath }) => modulePath.value.toString().split('?')[0].endsWith('.vue')) 56 | 57 | if (!importer.split('?')[0].endsWith('.vue') || parseResult.length === 0 || !parseResult.some(({ query }) => query.some(({ value }) => value.toString().trim() === 'async'))) { 58 | return 59 | } 60 | 61 | // 生成类型定义文件 62 | generateTypeFile(parseResult) 63 | 64 | const filename = removeExt(normalizeMiniProgramFilename(importer, inputDir)) 65 | 66 | const tempBindings: TemplateDescriptor['bindingAsyncComponents'] = {} 67 | 68 | const magicString = new MagicString(source) 69 | parseResult.forEach(({ full, fullPath, defaultVariable, modulePath, query }) => { 70 | const cache: Record = {} 71 | query.forEach(({ start, end, value }, index, list) => { 72 | const prevChar = source[start - 1] 73 | 74 | if (['async', ''].includes(value.toString().trim()) && (start !== end)) { 75 | magicString.overwrite(start, end, '') 76 | 77 | if (prevChar === '&') { 78 | magicString.overwrite(start - 1, start, '') 79 | } 80 | cache[index] = { start, end, value } 81 | 82 | // ---- 记录异步组件 [小程序环境下] ---- 83 | if (isMP) { 84 | const url = modulePath.value.toString() 85 | const realPath = getVitePathResolver()(url, true) 86 | // 根据调用主从关系,获取引用文件的相对路径 87 | let normalizedPath = calculateRelativePath(importer, realPath) 88 | // 去除 .vue 后缀 89 | normalizedPath = normalizedPath.replace(/\.vue$/, '') 90 | const tag = kebabCase(defaultVariable.value.toString()) 91 | tempBindings[tag] = AsyncComponentsInstance.generateBinding(tag, normalizedPath, realPath) 92 | } 93 | // ---- 记录异步组件 | 其他步骤是全平台的都要的,因为在 transform 阶段需要把 `import xxx from "*.vue?async"` 查询参数去除,否则会影响后续编译 ---- 94 | } 95 | }) 96 | 97 | if (cache[0]) { 98 | // 查找第一个不连续的数字之前的数字 99 | const flag = findFirstNonConsecutiveBefore(Object.keys(cache).map(Number)) 100 | 101 | const { start, end } = flag !== null ? query[flag + 1] : cache[0] 102 | const char = flag !== null ? '&' : '?' 103 | const prevChar = source[start - 1] 104 | if (prevChar === char) { 105 | magicString.overwrite(start - 1, start, '') 106 | } 107 | } 108 | }) 109 | 110 | // ---- 异步组件数据加入缓存 [小程序环境下] ---- 111 | if (isMP) { 112 | AsyncComponentsInstance.addScriptDescriptor(filename, tempBindings) 113 | AsyncComponentsInstance.addAsyncComponents(filename, tempBindings) 114 | } 115 | // ---- 异步组件数据加入缓存 ---- 116 | 117 | return { 118 | code: magicString.toString(), 119 | map: magicString.generateMap({ hires: true }), 120 | } 121 | }, 122 | generateBundle(_, bundle) { 123 | if (!isMP) 124 | return 125 | 126 | AsyncComponentsInstance.jsonAsyncComponentsCache.forEach((value, key) => { 127 | const chunk = bundle[`${key}.json`] 128 | // eslint-disable-next-line no-sequences 129 | const asyncComponents = Object.entries(value).reduce>((p, [key, value]) => (p[AsyncComponentsInstance.rename(key)] = value.value, p), {}) 130 | 131 | // 命中缓存,说明有需要处理的文件 | 注入`异步组件引用`配置 132 | if (chunk && chunk.type === 'asset' && AsyncComponentsInstance.jsonAsyncComponentsCache.get(key)) { 133 | // 读取 json 文件内容 | 没出错的话一定是 pages-json 134 | const jsonCode = JSON.parse(chunk.source.toString()) 135 | // 缓存原始page-json内容 136 | AsyncComponentsInstance.pageJsonCache.set(key, jsonCode) 137 | 138 | jsonCode.componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key, jsonCode.componentPlaceholder) 139 | 140 | jsonCode.usingComponents = Object.assign(jsonCode.usingComponents || {}, asyncComponents) 141 | chunk.source = JSON.stringify(jsonCode, null, 2) 142 | } 143 | else { 144 | let componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key) 145 | let usingComponents = asyncComponents 146 | const cache = AsyncComponentsInstance.pageJsonCache.get(key) 147 | 148 | if (cache) { 149 | usingComponents = Object.assign(cache.usingComponents || {}, usingComponents) 150 | componentPlaceholder = Object.assign(cache.componentPlaceholder || {}, componentPlaceholder) 151 | } 152 | 153 | bundle[`${key}.json`] = { 154 | type: 'asset', 155 | name: key, 156 | fileName: `${key}.json`, 157 | source: JSON.stringify(Object.assign({}, cache, { usingComponents, componentPlaceholder }), null, 2), 158 | } as typeof bundle.__proto__ 159 | } 160 | }) 161 | }, 162 | buildStart() { 163 | // 每次新的打包时,清空`异步组件`缓存,主要避免热更新时的缓存问题 164 | AsyncComponentsInstance.jsonAsyncComponentsCache.clear() 165 | AsyncComponentsInstance.scriptDescriptors.clear() 166 | }, 167 | } 168 | } 169 | 170 | export default AsyncComponentProcessor 171 | 172 | /** 173 | * 生成类型定义 174 | */ 175 | function generateModuleDeclaration(parsedResults?: ReturnType, cache?: string[]): string { 176 | let typeDefs = '' 177 | 178 | const prefixList = [ 179 | '/* eslint-disable */', 180 | '/* prettier-ignore */', 181 | '// @ts-nocheck', 182 | '// Generated by @uni-ku/bundle-optimizer', 183 | 'declare module \'*?async\' {', 184 | ' const component: any', 185 | ' export = component', 186 | '}', 187 | ] 188 | prefixList.forEach((prefix) => { 189 | typeDefs += `${prefix}\n` 190 | }) 191 | 192 | // 生成 declare module 语句 193 | function generateDeclareModule(modulePath: string | number, fullPath: string | number) { 194 | typeDefs += `declare module '${fullPath}' {\n` 195 | typeDefs += ` const component: typeof import('${modulePath}')\n` 196 | typeDefs += ` export = component\n` 197 | typeDefs += `}\n` 198 | } 199 | 200 | cache?.forEach((item) => { 201 | const modulePath = item // 模块路径 202 | const fullPath = `${modulePath}?async` 203 | 204 | generateDeclareModule(modulePath, fullPath) 205 | }) 206 | 207 | parsedResults?.filter(item => !cache?.includes(item.modulePath.value.toString())) 208 | .forEach((result) => { 209 | const modulePath = result.modulePath.value // 模块路径 210 | const fullPath = result.fullPath.value 211 | 212 | generateDeclareModule(modulePath, fullPath) 213 | }) 214 | 215 | return typeDefs 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @uni-ku/bundle-optimizer npm package 2 | 3 | [![NPM downloads](https://img.shields.io/npm/dm/@uni-ku/bundle-optimizer?label=downloads)](https://www.npmjs.com/package/@uni-ku/bundle-optimizer) 4 | [![LICENSE](https://img.shields.io/github/license/uni-ku/bundle-optimizer?style=flat&label=license)](https://github.com/uni-ku/bundle-optimizer#readme) 5 | [![pkg.pr.new](https://pkg.pr.new/badge/uni-ku/bundle-optimizer?style=flat&color=000&logoSize=auto)](https://pkg.pr.new/~/uni-ku/bundle-optimizer) 6 | 7 | > [!TIP] 8 | > uni-app 分包优化插件化实现 9 | > 10 | > 前往 查看本项目立项背景。 11 | > 12 | > 前往 查看本插件详细发展过程与提交记录。 13 | 14 | ### 🎏 功能与支持 15 | 16 | > !暂时没有对App平台做兼容性实现 17 | > 18 | > 适用于 Uniapp - CLI 或 HBuilderX 创建的 Vue3 项目 19 | 20 | - 分包优化 21 | - 模块异步跨包调用 22 | - 组件异步跨包引用 23 | 24 | ### 📦 安装 25 | 26 | ```bash 27 | pnpm add -D @uni-ku/bundle-optimizer 28 | ``` 29 | 30 | ### 🚀 使用 31 | 32 | #### 0. 插件可配置参数 33 | 34 | > !以下各参数均为可选参数,默认开启所有插件功能,并在项目根目录下生成`async-import.d.ts`与`async-component.d.ts`文件 35 | 36 | |参数-[enable]|类型|默认值|描述| 37 | |---|---|---|---| 38 | |enable|`boolean`\|`object`|`true`|插件功能总开关,`object`时可详细配置各插件启闭状态,详见下列| 39 | |enable.optimization|`boolean`|`true`|分包优化启闭状态| 40 | |enable['async-import']|`boolean`|`true`|模块异步跨包调用启闭状态| 41 | |enable['async-component']|`boolean`|`true`|组件异步跨包引用启闭状态| 42 | 43 | |参数-[dts]|类型|默认值|描述| 44 | |---|---|---|---| 45 | |dts|`boolean`\|`object`|`true`|dts文件输出总配置,`true`时按照下列各配置的默认参数来(根目录下生成`async-import.d.ts`与`async-component.d.ts`文件),`object`时可详细配置各类型文件的生成,详见下列| 46 | |dts.enable|`boolean`|`true`|总配置,是否生成dts文件| 47 | |dts.base|`string`|`./`|总配置,dts文件输出目录,可相对路径,也可绝对路径| 48 | |dts['async-import']|`boolean`\|`object`|`true`|`async-import`dts文件配置,默认为`true`(在项目根目录生成`async-import.d.ts`文件),`object`时可详细配置该项的生成| 49 | |dts['async-import'].enable|`boolean`|`true`|是否生成dts文件| 50 | |dts['async-import'].base|`string`|`./`|dts文件输出目录,可相对路径,也可绝对路径| 51 | |dts['async-import'].name|`string`|`async-import.d.ts`|dts文件名称,需要包含文件后缀| 52 | |dts['async-import'].path|`string`|`${base}/${name}`|dts文件输出路径,如果没有定义此项则会是`${base}/${name}`,否则此配置项优先级更高,可相对路径,也可绝对路径| 53 | |dts['async-component']|`boolean`\|`object`|`true`|`async-component`dts文件配置,默认为`true`(在项目根目录生成`async-component.d.ts`文件),`object`时可详细配置该项的生成| 54 | |dts['async-component'].enable|`boolean`|`true`|是否生成dts文件| 55 | |dts['async-component'].base|`string`|`./`|dts文件输出目录,可相对路径,也可绝对路径| 56 | |dts['async-component'].name|`string`|`async-component.d.ts`|dts文件名称,需要包含文件后缀| 57 | |dts['async-component'].path|`string`|`${base}/${name}`|dts文件输出路径,如果没有定义此项则会是`${base}/${name}`,否则此配置项优先级更高,可相对路径,也可绝对路径| 58 | 59 | |参数-[logger]|类型|默认值|描述| 60 | |---|---|---|---| 61 | |logger|`boolean`\|`string[]`|`false`|插件日志输出总配置,`true`时启用所有子插件的日志功能;`string[]`时可具体启用部分插件的日志,可以是`optimization`、`async-component`、`async-import`| 62 | 63 | #### 1. 引入 `@uni-ku/bundle-optimizer` 64 | 65 | - CLI: `直接编写` 根目录下的 vite.config.* 66 | - HBuilderX: 需要根据你所使用语言, 在根目录下 `创建` vite.config.* 67 | 68 | ##### 简单配置: 69 | 70 | ```js 71 | // vite.config.* 72 | import Uni from '@dcloudio/vite-plugin-uni' 73 | import Optimization from '@uni-ku/bundle-optimizer' 74 | import { defineConfig } from 'vite' 75 | 76 | export default defineConfig({ 77 | plugins: [ 78 | Uni(), 79 | Optimization({ 80 | enable: { 81 | 'optimization': true, 82 | 'async-import': true, 83 | 'async-component': true, 84 | }, 85 | dts: { 86 | enable: true, 87 | base: './', 88 | }, 89 | logger: true, 90 | }), 91 | ], 92 | }) 93 | ``` 94 | 95 | ##### 详细配置说明 96 | 97 | ```js 98 | // vite.config.* 99 | import Uni from '@dcloudio/vite-plugin-uni' 100 | import Optimization from '@uni-ku/bundle-optimizer' 101 | import { defineConfig } from 'vite' 102 | 103 | export default defineConfig({ 104 | plugins: [ 105 | Uni(), 106 | // 可以无需传递任何参数,默认开启所有插件功能,并在项目根目录生成类型定义文件 107 | Optimization({ 108 | // 插件功能开关,默认为true,即开启所有功能 109 | enable: { 110 | 'optimization': true, 111 | 'async-import': true, 112 | 'async-component': true, 113 | }, 114 | // dts文件输出配置,默认为true,即在项目根目录生成类型定义文件 115 | dts: { 116 | 'enable': true, 117 | 'base': './', 118 | // 上面是对类型生成的比较全局的一个配置 119 | // 下面是对每个类型生成的配置,以下各配置均为可选参数 120 | 'async-import': { 121 | enable: true, 122 | base: './', 123 | name: 'async-import.d.ts', 124 | path: './async-import.d.ts', 125 | }, 126 | 'async-component': { 127 | enable: true, 128 | base: './', 129 | name: 'async-component.d.ts', 130 | path: './async-component.d.ts', 131 | }, 132 | }, 133 | // 也可以传递具体的子插件的字符串列表,如 ['optimization', 'async-import', 'async-component'],开启部分插件的log功能 134 | logger: true, 135 | }), 136 | ], 137 | }) 138 | ``` 139 | 140 | #### 2. 修改 `manifest.json` 141 | 142 | 需要修改 manifest.json 中的 `mp-weixin.optimization.subPackages` 配置项为 true,开启方法与vue2版本的uniapp一致。 143 | 144 | ```json 145 | { 146 | "mp-weixin": { 147 | "optimization": { 148 | "subPackages": true 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | > 使用了 `@uni-helper/vite-plugin-uni-manifest` 的项目,修改 `manifest.config.ts` 的对应配置项即可。 155 | 156 | #### 3. 将插件生成的类型标注文件加入 `tsconfig.json` 157 | 158 | 插件运行时默认会在项目根目录下生成 `async-import.d.ts` 与 `async-component.d.ts` 两个类型标注文件,需要将其加入到 `tsconfig.json` 的 `include` 配置项中;如果有自定义dts生成路径,则根据实际情况填写。 159 | 160 | 当然,如果原来的配置已经覆盖到了这两个文件,就可以不加;如果没有运行项目的时候,这两个文件不会生成。 161 | 162 | ```json 163 | { 164 | "include": [ 165 | "async-import.d.ts", 166 | "async-component.d.ts" 167 | ] 168 | } 169 | ``` 170 | 171 | - `async-import.d.ts`:定义了 `AsyncImport` 这个异步函数,用于异步引入模块。 172 | - `async-component.d.ts`:拓展了 `import` 的 `静态引入`,引入路径后面加上`?async`即可实现小程序端的组件异步引用。 173 | - **详见 ** 174 | 175 | > 这两个类型文件不会对项目的运行产生任何影响,只是为了让编辑器能够正确的识别本插件定义的自定义语法、类型。 176 | > 177 | > 这两个文件可以加入到 `.gitignore` 中,不需要提交到代码仓库。 178 | 179 | ### ✨ 例子 180 | 181 | > 以下例子均以CLI创建项目为例, HBuilderX 项目与以上设置同理 ~~, 只要注意是否需要包含 src目录 即可~~。 182 | > 183 | > 现在已经支持 hbx 创建的 vue3 + vite、不以 src 为主要代码目录的项目。 184 | 185 | 🔗 [查看以下例子的完整项目](./examples) 186 | 187 |
188 | 189 | 1. (点击展开) 分包优化 190 | 191 |
192 | 193 | `分包优化` 是本插件运行时默认开启的功能,无需额外配置,只需要确认 `manifest.json` 中的 `mp-weixin.optimization.subPackages` 配置项为 true 即可。 194 | 195 | 详情见本文档中的 [`使用`](#-使用) 部分。 196 | 197 |
198 | 199 |
200 | 201 | 2. (点击展开) 模块异步跨包调用 202 | 203 |
204 | 205 | - `模块异步跨包调用` 是指在一个分包中引用另一个分包中的模块(不限主包与分包),这里的模块可以是 js/ts 模块(插件)、vue 文件。当然,引入 vue 文件一般是没有什么意义的,但是也做了兼容处理。 206 | - `TODO:` 是否支持 json 文件? 207 | 208 | 可以使用函数 `AsyncImport` 这个异步函数来实现模块的异步引入。 209 | 210 | ```js 211 | // js/ts 模块(插件) 异步引入 212 | await AsyncImport('@/pages-sub-async/async-plugin/index').then((res) => { 213 | console.log(res?.AsyncPlugin()) // 该插件导出了一个具名函数 214 | }) 215 | 216 | // vue 文件 异步引入(页面文件) 217 | AsyncImport('@/pages-sub-async/index.vue').then((res) => { 218 | console.log(res.default || res) 219 | }) 220 | 221 | // vue 文件 异步引入(组件文件) 222 | AsyncImport('@/pages-sub-async/async-component/index.vue').then((res) => { 223 | console.log(res.default || res) 224 | }) 225 | ``` 226 | 227 |
228 | 229 |
230 | 231 | 3. (点击展开) 组件异步跨包引用 232 | 233 |
234 | 235 | - `组件异步跨包引用` 是指在一个分包中引用另一个分包中的组件(不限主包与分包),这里的组件就是 vue 文件;貌似支持把页面文件也作为组件引入。 236 | - 在需要跨包引入的组件路径后面加上 `?async` 即可实现异步引入。 237 | 238 | ```vue 239 | 242 | 243 | 248 | ``` 249 | 250 |
251 | 252 | ### 🏝 周边 253 | 254 | |项目|描述| 255 | |---|---| 256 | |[Uni Ku](https://github.com/uni-ku)|有很多 Uniapp(Uni) 的酷(Ku) 😎| 257 | |[create-uni](https://uni-helper.js.org/create-uni)|🛠️ 快速创建uni-app项目| 258 | |[Wot Design Uni](https://github.com/Moonofweisheng/wot-design-uni/)|一个基于Vue3+TS开发的uni-app组件库,提供70+高质量组件| 259 | 260 | ### 🧔 找到我 261 | 262 | > 加我微信私聊,方便定位、解决问题。 263 | 264 | 265 | 266 | 270 | 271 |
267 | wechat-qrcode 268 |

微信

269 |
272 | 273 | ### 💖 赞赏 274 | 275 | 如果我的工作帮助到了您,可以请我吃辣条,使我能量满满 ⚡ 276 | 277 | > 请留下您的Github用户名,感谢 ❤ 278 | 279 | #### 直接赞助 280 | 281 | 282 | 283 | 287 | 291 | 292 |
284 | wechat-pay 285 |

微信

286 |
288 | alipay 289 |

支付宝

290 |
293 | 294 | #### 赞赏榜单 295 | 296 |

297 | 298 | sponsors 299 | 300 |

301 | 302 | --- 303 | 304 |

305 | Happy coding! 306 |

307 | -------------------------------------------------------------------------------- /src/plugin/template.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 12 | 135 | 136 | 137 |
138 |

139 | Last updated: {{ now() | date('toLocaleString') }} 140 | {% if isDevServer %} 141 | - Changes will be reflected on next data fetch. 142 | {% else %} 143 | - Rebuild project to see changes. 144 | {% endif %} 145 |

146 | Switch selectedMode: 147 | 151 |
152 | Switch graphLayout: 153 | 158 |
159 |
160 | 161 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /src/plugin/subpackages-optimization.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable unused-imports/no-unused-vars */ 3 | /* eslint-disable node/prefer-global/process */ 4 | import type { Plugin } from 'vite' 5 | import type { ISubPkgsInfo, ManualChunkMeta, ManualChunksOption, ModuleInfo } from '../type' 6 | import fs from 'node:fs' 7 | import path from 'node:path' 8 | import { parseManifestJsonOnce, parseMiniProgramPagesJson } from '@dcloudio/uni-cli-shared' 9 | import { logger } from '../common/Logger' 10 | import { EXT_RE, EXTNAME_JS_RE, ROOT_DIR } from '../constants' 11 | import { moduleIdProcessor as _moduleIdProcessor, normalizePath, parseQuerystring } from '../utils' 12 | import { parseVirtualPath } from '../utils/uniapp' 13 | 14 | /** 15 | * uniapp 分包优化插件 16 | */ 17 | export function SubPackagesOptimization(enableLogger: boolean): Plugin { 18 | const platform = process.env.UNI_PLATFORM 19 | const inputDir = process.env.UNI_INPUT_DIR 20 | 21 | if (!platform || !inputDir) { 22 | throw new Error('`UNI_INPUT_DIR` or `UNI_PLATFORM` is not defined') 23 | } 24 | 25 | // #region 分包优化参数获取 26 | const manifestJson = parseManifestJsonOnce(inputDir) 27 | const platformOptions = manifestJson[platform] || {} 28 | const optimization = platformOptions.optimization || {} 29 | process.env.UNI_OPT_TRACE = `${!!optimization.subPackages}` 30 | 31 | const pagesJsonPath = path.resolve(inputDir, 'pages.json') 32 | const jsonStr = fs.readFileSync(pagesJsonPath, 'utf8') 33 | const { appJson } = parseMiniProgramPagesJson(jsonStr, platform, { subpackages: true }) 34 | 35 | const pagesFlat = { 36 | pages: appJson.pages || [], 37 | subPackages: (appJson.subPackages || []).flatMap((pkg) => { 38 | return pkg.pages.map(page => `${pkg.root}/${page}`.replace(/\/{2,}/g, '/')) 39 | }), 40 | get all() { 41 | return [...this.pages, ...this.subPackages] 42 | }, 43 | } 44 | 45 | logger.info(`pagesFlat: ${JSON.stringify(pagesFlat, null, 2)}`, true) 46 | 47 | process.UNI_SUBPACKAGES = appJson.subPackages || {} 48 | // #endregion 49 | 50 | // #region subpackage 51 | const UNI_SUBPACKAGES = process.UNI_SUBPACKAGES || {} 52 | const subPkgsInfo: ISubPkgsInfo[] = Object.values(UNI_SUBPACKAGES) 53 | const normalFilter = ({ independent }: ISubPkgsInfo) => !independent 54 | const independentFilter = ({ independent }: ISubPkgsInfo) => independent 55 | /** 先去除尾部的`/`,再添加`/`,兼容pages.json中以`/`结尾的路径 */ 56 | const map2Root = ({ root }: ISubPkgsInfo) => `${root.replace(/\/$/, '')}/` 57 | const subPackageRoots = subPkgsInfo.map(map2Root) 58 | const normalSubPackageRoots = subPkgsInfo.filter(normalFilter).map(map2Root) 59 | const independentSubpackageRoots = subPkgsInfo.filter(independentFilter).map(map2Root) 60 | 61 | /** 62 | * # id处理器 63 | * @description 将 moduleId 转换为相对于 inputDir 的路径并去除查询参数后缀 64 | */ 65 | function moduleIdProcessor(id: string, removeQuery = true) { 66 | return _moduleIdProcessor(id, process.env.UNI_INPUT_DIR, removeQuery) 67 | } 68 | /** 69 | * # id处理器 70 | * @description 将 moduleId 转换为相对于 rootDir 的路径并去除查询参数后缀 71 | */ 72 | function moduleIdProcessorForRoot(id: string, removeQuery = true) { 73 | return _moduleIdProcessor(id, undefined, removeQuery) 74 | } 75 | 76 | /** 77 | * 判断该文件模块的来源 78 | */ 79 | const moduleFrom = (id: string): 80 | { from: 'main' | 'node_modules', clearId: string } 81 | | { from: 'sub', clearId: string, pkgRoot: string } 82 | | undefined => { 83 | let root = normalizePath(ROOT_DIR) 84 | if (!root.endsWith('/')) 85 | root = `${root}/` 86 | 87 | const clearId = moduleIdProcessor(id) 88 | 89 | if (!path.isAbsolute(clearId)) { 90 | const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(clearId).indexOf(root) === 0) 91 | if (pkgRoot === undefined) 92 | return { from: clearId.startsWith('node_modules/') ? 'node_modules' : 'main', clearId } 93 | else 94 | return { from: 'sub', clearId, pkgRoot } 95 | } 96 | else { 97 | // clearId.startsWith(root) && TODO: 放宽条件,兼容 workspace 项目 98 | if (clearId.includes('/node_modules/')) 99 | return { from: 'node_modules', clearId } 100 | } 101 | } 102 | 103 | /** 查找模块列表中是否有属于子包的模块 */ 104 | const findSubPackages = function (importers: readonly string[]) { 105 | return importers.reduce((pkgs, item) => { 106 | const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(item).indexOf(root) === 0) 107 | pkgRoot && pkgs.add(pkgRoot) 108 | return pkgs 109 | }, new Set()) 110 | } 111 | 112 | /** 判断是否有非子包的import (是否被非子包引用) */ 113 | const hasNoSubPackage = function (importers: readonly string[]) { 114 | return importers.some((item) => { 115 | // 遍历所有的子包根路径,如果模块的路径不包含子包路径,就说明被非子包引用了 116 | return !subPackageRoots.some(root => moduleIdProcessor(item).indexOf(root) === 0) 117 | }) 118 | } 119 | /** 查找来自 主包 下的依赖 */ 120 | const findMainPackage = function (importers: readonly string[]) { 121 | const list = importers.filter((item) => { 122 | const id = moduleIdProcessor(item) 123 | // 排除掉子包和第三方包之后,剩余的视为主包 124 | return !subPackageRoots.some(root => id.indexOf(root) === 0) && !id.includes('node_modules') 125 | }) 126 | return list 127 | } 128 | /** 查找`node_modules`下的三方依赖 */ 129 | const findNodeModules = function (importers: readonly string[]) { 130 | const mainPackageList = findMainPackage(importers) 131 | return importers.filter((item) => { 132 | const id = moduleIdProcessor(item) 133 | // 排除主包和子包,并且包含“node_modules” 134 | return !mainPackageList.includes(item) && !subPackageRoots.some(root => id.indexOf(root) === 0) && id.includes('node_modules') 135 | }) 136 | } 137 | /** 查找三方依赖的组件库 */ 138 | const findNodeModulesComponent = function (importers: readonly string[]) { 139 | const list = findNodeModules(importers) 140 | const nodeModulesComponent = new Set(list 141 | .map(item => moduleIdProcessor(item)) 142 | .filter(name => name.endsWith('.vue') || name.endsWith('.nvue'))) 143 | return nodeModulesComponent 144 | } 145 | 146 | /** 查找来自 主包 下的组件 */ 147 | const findMainPackageComponent = function (importers: readonly string[]) { 148 | const list = findMainPackage(importers) 149 | const mainPackageComponent = new Set(list 150 | .map(item => moduleIdProcessor(item)) 151 | .filter(name => name.endsWith('.vue') || name.endsWith('.nvue'))) 152 | return mainPackageComponent 153 | } 154 | /** 判断是否含有项目入口文件的依赖 */ 155 | const hasEntryFile = function (importers: readonly string[], meta: ManualChunkMeta) { 156 | const list = findMainPackage(importers) 157 | return list.some(item => meta.getModuleInfo(item)?.isEntry) 158 | } 159 | /** 判断该模块引用的模块是否有跨包引用的组件 */ 160 | const hasMainPackageComponent = function (moduleInfo: Partial, subPackageRoot?: string) { 161 | if (moduleInfo.id && moduleInfo.importedIdResolutions) { 162 | for (let index = 0; index < moduleInfo.importedIdResolutions.length; index++) { 163 | const m = moduleInfo.importedIdResolutions[index] 164 | 165 | if (m && m.id) { 166 | const name = moduleIdProcessor(m.id) 167 | // 判断是否为组件 168 | if (name.includes('.vue') || name.includes('.nvue')) { 169 | // 判断存在跨包引用的情况(该组件的引用路径不包含子包路径,就说明跨包引用了) 170 | if (subPackageRoot && !name.includes(subPackageRoot)) { 171 | if (process.env.UNI_OPT_TRACE) { 172 | console.log('move module to main chunk:', moduleInfo.id, 'from', subPackageRoot, 'for component in main package:', name) 173 | } 174 | 175 | // 独立分包除外 176 | const independentRoot = independentSubpackageRoots.find(root => name.includes(root)) 177 | if (!independentRoot) { 178 | return true 179 | } 180 | } 181 | } 182 | else { 183 | return hasMainPackageComponent(m, subPackageRoot) 184 | } 185 | } 186 | } 187 | } 188 | return false 189 | } 190 | 191 | /** 192 | * 判断模块是否是一个 vue 文件的 script 函数模块 193 | * @deprecated 弃用,使用 isVueEntity:一旦 vue 实体文件确定编译去向之后,其关联的 css\js 会自动跟随 194 | */ 195 | const isVueScript = (moduleInfo: Partial) => { 196 | if (!moduleInfo.id || !moduleInfo.importers?.length) { 197 | return false 198 | } 199 | const importer = moduleIdProcessor(moduleInfo.importers[0]) 200 | const id = moduleInfo.id 201 | const clearId = moduleIdProcessor(id, false) 202 | 203 | const parsedUrl = parseQuerystring(clearId) 204 | 205 | return parsedUrl && parsedUrl.type === 'script' && parsedUrl.vue && importer === moduleIdProcessor(id) 206 | } 207 | 208 | /** 判断模块是否是一个 vue 文件本体 */ 209 | const isVueEntity = (moduleInfo: Partial) => { 210 | if (!moduleInfo.id || !moduleInfo.importers?.length || !moduleInfo.id.endsWith('.vue')) { 211 | return false 212 | } 213 | const clearId = moduleIdProcessor(moduleInfo.id) 214 | // info: 判断 importers 是否存在一个是虚拟组件(与当前moduleInfo.id一致) 215 | return moduleInfo.importers.some((importer) => { 216 | const [is, real, _type] = parseVirtualPath(importer) 217 | return is && [moduleInfo.id, clearId].includes(real) 218 | }) 219 | } 220 | // #endregion 221 | 222 | logger.info('[optimization] 分包优化插件已启用', !enableLogger) 223 | 224 | return { 225 | name: 'uniapp-subpackages-optimization', 226 | enforce: 'post', // 控制执行顺序,post 保证在其他插件之后执行 227 | config(config, { command }) { 228 | if (!platform.startsWith('mp')) { 229 | logger.warn('[optimization] 分包优化插件仅需在小程序平台启用,跳过', !enableLogger) 230 | return 231 | } 232 | 233 | const UNI_OPT_TRACE = process.env.UNI_OPT_TRACE === 'true' 234 | logger.info(`[optimization] 分包优化开启状态: ${UNI_OPT_TRACE}`, !true) // !!! 此处始终开启log 235 | if (!UNI_OPT_TRACE) 236 | return 237 | 238 | const originalOutput = config?.build?.rollupOptions?.output 239 | 240 | const existingManualChunks 241 | = (Array.isArray(originalOutput) ? originalOutput[0]?.manualChunks : originalOutput?.manualChunks) as ManualChunksOption 242 | 243 | // 合并已有的 manualChunks 配置 244 | const mergedManualChunks: ManualChunksOption = (id, meta) => { 245 | /** 依赖图谱分析 */ 246 | function getDependencyGraph(startId: string, getRelated: (info: ModuleInfo) => readonly string[] = info => info.importers): string[] { 247 | const visited = new Set() 248 | const result: string[] = [] 249 | 250 | // 支持自定义遍历方向 251 | function traverse( 252 | currentId: string, 253 | getRelated: (info: ModuleInfo) => readonly string[], // 控制遍历方向的回调函数 254 | ) { 255 | if (visited.has(currentId)) 256 | return 257 | 258 | visited.add(currentId) 259 | result.push(currentId) 260 | 261 | const moduleInfo = meta.getModuleInfo(currentId) 262 | if (!moduleInfo) 263 | return 264 | 265 | getRelated(moduleInfo).forEach((relatedId) => { 266 | traverse(relatedId, getRelated) 267 | }) 268 | } 269 | 270 | // 默认:向上追踪 importers(谁导入了当前模块) 271 | traverse(startId, getRelated) 272 | 273 | // 若需要向下追踪 dependencies(当前模块导入了谁): 274 | // traverse(startId, (info) => info.dependencies); 275 | 276 | return result 277 | } 278 | 279 | const normalizedId = normalizePath(id) 280 | const filename = normalizedId.split('?')[0] 281 | 282 | let mainFlag: false | string = false 283 | 284 | // #region ⚠️ 以下代码是分包优化的核心逻辑 285 | // 处理项目内的js,ts文件 | 兼容 json 文件,import json 会被处理成 js 模块 286 | if (EXTNAME_JS_RE.test(filename) && (filename.startsWith(normalizePath(inputDir)) || filename.includes('node_modules'))) { 287 | // 如果这个资源只属于一个子包,并且其调用组件的不存在跨包调用的情况,那么这个模块就会被加入到对应的子包中。 288 | const moduleInfo = meta.getModuleInfo(id) 289 | if (!moduleInfo) { 290 | throw new Error(`moduleInfo is not found: ${id}`) 291 | } 292 | 293 | const importersGraph = getDependencyGraph(id) // 搜寻引用图谱 294 | const newMatchSubPackages = findSubPackages(importersGraph) 295 | // 查找引用图谱中是否有主包的组件文件模块 296 | const newMainPackageComponent = findMainPackageComponent(importersGraph) 297 | // 查找三方依赖组件库 298 | const nodeModulesComponent = findNodeModulesComponent(importersGraph) 299 | /** 300 | * 是否有被项目入口文件直接引用 301 | */ 302 | const isEntry = hasEntryFile(importersGraph, meta) 303 | 304 | // 引用图谱中只找到一个子包的引用,并且没有出现主包的组件以及入口文件(main.{ts|js}),且没有被三方组件库引用,则说明只归属该子包 305 | if (!isEntry && newMatchSubPackages.size === 1 && newMainPackageComponent.size === 0 && nodeModulesComponent.size === 0) { 306 | logger.info(`[optimization] 子包: ${[...newMatchSubPackages].join(', ')} <- ${filename}`, !enableLogger) 307 | return `${newMatchSubPackages.values().next().value}common/vendor` 308 | } 309 | mainFlag = id 310 | } 311 | // #endregion 312 | 313 | // 调用已有的 manualChunks 配置 | 此处必须考虑到原有的配置,是为了使 uniapp 原本的分包配置生效 314 | if (existingManualChunks && typeof existingManualChunks === 'function') { 315 | const result = existingManualChunks(id, meta) 316 | 317 | if (result === undefined) { 318 | const moduleInfo = meta.getModuleInfo(id) 319 | 320 | if (moduleInfo) { 321 | // 当 UNI_INPUT_DIR 和 VITE_ROOT_DIR 一致时,clearId 和 clearIdForRoot 是一致的 322 | // hbx 创建的没有 src 的目录,就是一致的情况 323 | // 其余情况,clearIdForRoot 是相对路径的情况下,clearId 可能是绝对路径 324 | const clearIdForRoot = moduleIdProcessorForRoot(moduleInfo.id) 325 | const clearId = moduleIdProcessor(moduleInfo.id) 326 | 327 | if (mainFlag === id && !moduleInfo.isEntry && !findNodeModules([moduleInfo.id]).length) { 328 | logger.info(`[optimization] 主包内容强制落盘: ${clearId}`, !enableLogger) 329 | return clearId 330 | } 331 | 332 | // TODO: 绝对路径是 monorepo 项目结构下的三方依赖库的特点,或者其他情况,这里暂时不做处理 333 | if (isVueEntity(moduleInfo) && !path.isAbsolute(clearIdForRoot)) { 334 | const targetId = path.isAbsolute(clearId) ? clearIdForRoot : clearId 335 | const originalTarget = targetId.replace(EXT_RE, '') 336 | // 规整没处理好的 vue 实体模块 337 | // uniapp 会将三方库落盘路径 node_modules 改为 node-modules 338 | // TODO: 需要对此类业务总结、抽离 339 | const target = originalTarget.replace(/^(\.?\/)?node_modules\//, 'node-modules/') 340 | logger.info(`[optimization] 规整 vue 实体模块: ${originalTarget} -> ${target}-vendor`, !enableLogger) 341 | return `${target}-vendor` 342 | } 343 | } 344 | } 345 | 346 | logger.warn(`[optimization] default: ${result} <- ${id}`, !enableLogger) 347 | return result 348 | } 349 | } 350 | 351 | return { 352 | build: { 353 | rollupOptions: { 354 | output: { 355 | manualChunks: mergedManualChunks, 356 | }, 357 | }, 358 | }, 359 | } 360 | }, 361 | } 362 | } 363 | 364 | export default SubPackagesOptimization 365 | --------------------------------------------------------------------------------