├── .commitlintrc.yaml ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .vscode ├── schema │ ├── commitlint-patch.json │ ├── commitlint-with-cz.json │ ├── commitlint.json │ ├── cz-git.json │ └── readme.md └── settings.json ├── LICENSE ├── README.md ├── build.config.ts ├── eslint.config.ts ├── examples ├── hbx+vue3 │ ├── App.vue │ ├── api │ │ └── index.js │ ├── index.html │ ├── lib │ │ └── base-request.js │ ├── main.js │ ├── manifest.json │ ├── package.json │ ├── pages-sub │ │ ├── api │ │ │ └── index.js │ │ ├── index │ │ │ └── index.vue │ │ └── plugins │ │ │ └── index.js │ ├── pages.json │ ├── pages │ │ └── index │ │ │ └── index.vue │ ├── static │ │ └── logo.png │ ├── types │ │ ├── async-component.d.ts │ │ └── async-import.d.ts │ ├── uni.promisify.adaptor.js │ ├── uni.scss │ └── vite.config.js └── vue3+vite+ts │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ ├── api │ │ ├── index.ts │ │ ├── test.d.ts │ │ └── test.js │ ├── lib │ │ ├── base-request.ts │ │ └── demo.ts │ ├── main.ts │ ├── manifest.json │ ├── pages-sub-async │ │ ├── component.vue │ │ ├── index.vue │ │ └── plugin.ts │ ├── pages-sub-demo │ │ ├── api │ │ │ └── index.ts │ │ ├── components │ │ │ └── demo1.vue │ │ └── index.vue │ ├── pages.json │ └── pages │ │ └── index.vue │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── async-component.ts ├── async-import.ts ├── common │ ├── AsyncComponents.ts │ ├── AsyncImports.ts │ ├── Logger.ts │ ├── PackageModules.ts │ └── ParseOptions.ts ├── constants.ts ├── env.d.ts ├── index.ts ├── main.ts ├── plugin │ ├── async-component-processor.ts │ ├── async-import-processor.ts │ └── vite-plugin-global-method.ts ├── type.d.ts └── utils │ ├── crypto │ ├── base_encode.ts │ ├── index.ts │ ├── readme │ └── xxhash.ts │ ├── getViteConfigPaths.ts │ ├── index.ts │ └── lex-parse │ ├── index.ts │ ├── parse_arguments.ts │ ├── parse_import.ts │ ├── readme │ └── type.d.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build Outputs 5 | build 6 | dist -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/schema/cz-git.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "#/definitions/CommitizenGitOptions", 4 | "definitions": { 5 | "CommitizenGitOptions": { 6 | "type": "object", 7 | "properties": { 8 | "path": { 9 | "type": "string", 10 | "description": "project: \"node_modules/cz-git\" root: \"cz-git\"", 11 | "default": "node_modules/cz-git" 12 | }, 13 | "$schema": { 14 | "type": "string" 15 | }, 16 | "prompt": { 17 | "$ref": "#/definitions/CommitizenGitOptions" 18 | }, 19 | "alias": { 20 | "type": "object", 21 | "additionalProperties": { 22 | "type": "string" 23 | }, 24 | "description": "define commonly used commit message alias", 25 | "default": { 26 | "fd": "docs: fix typos" 27 | } 28 | }, 29 | "messages": { 30 | "$ref": "#/definitions/Answers", 31 | "description": "Customize prompt questions" 32 | }, 33 | "themeColorCode": { 34 | "type": "string", 35 | "description": "the prompt inquirer primary color", 36 | "examples": [ 37 | "38;5;043" 38 | ], 39 | "default": "" 40 | }, 41 | "types": { 42 | "type": "array", 43 | "items": { 44 | "$ref": "#/definitions/TypesOption" 45 | }, 46 | "description": "Customize prompt type" 47 | }, 48 | "typesAppend": { 49 | "type": "array", 50 | "items": { 51 | "$ref": "#/definitions/TypesOption" 52 | }, 53 | "description": "Add extra types to default types", 54 | "default": [] 55 | }, 56 | "typesSearchValue": { 57 | "type": "boolean", 58 | "description": "Default types list fuzzy search types `value` key of list. If choose `false` will search `name` key of list.", 59 | "default": true 60 | }, 61 | "useAI": { 62 | "type": "boolean", 63 | "description": "Use OpenAI to auto generate short description for commit message", 64 | "default": false 65 | }, 66 | "aiNumber": { 67 | "type": "number", 68 | "description": "If >1 will turn on select mode, select generate options like returned by OpenAI", 69 | "default": 1 70 | }, 71 | "aiModel": { 72 | "type": "string", 73 | "description": "Choose the AI model you want to use", 74 | "default": "gpt-4o-mini" 75 | }, 76 | "aiDiffIgnore": { 77 | "type": "array", 78 | "items": { 79 | "type": "string" 80 | }, 81 | "description": "To ignore selection codes when sending AI API requests", 82 | "examples": [ 83 | [ 84 | "pnpm-lock.yaml", 85 | "docs/public" 86 | ] 87 | ], 88 | "default": [ 89 | "package-lock.json", 90 | "yarn.lock", 91 | "pnpm-lock.yaml" 92 | ] 93 | }, 94 | "useEmoji": { 95 | "type": "boolean", 96 | "description": "Use emoji ?It will be use typesOption.emoji code", 97 | "default": false 98 | }, 99 | "emojiAlign": { 100 | "type": "string", 101 | "enum": [ 102 | "left", 103 | "center", 104 | "right" 105 | ], 106 | "description": "Set the location of emoji in header", 107 | "default": "center" 108 | }, 109 | "scopes": { 110 | "$ref": "#/definitions/ScopesType", 111 | "description": "Provides a select of prompt to select module scopes" 112 | }, 113 | "scopesSearchValue": { 114 | "type": "boolean", 115 | "description": "Default scope list fuzzy search types `name` key of list. If choose `true` will search `value` key of list.", 116 | "default": false 117 | }, 118 | "scopeOverrides": { 119 | "type": "object", 120 | "additionalProperties": { 121 | "$ref": "#/definitions/ScopesType" 122 | }, 123 | "description": "Provides an overriding select of prompt to select module scopes under specific type", 124 | "examples": [ 125 | { 126 | "test": [ 127 | "e2eTest", 128 | "unitTest" 129 | ] 130 | } 131 | ] 132 | }, 133 | "scopeFilters": { 134 | "type": "array", 135 | "items": { 136 | "type": "string" 137 | }, 138 | "description": "Filter select of prompt to select module scopes by the scope.value", 139 | "default": [ 140 | ".DS_Store" 141 | ] 142 | }, 143 | "enableMultipleScopes": { 144 | "type": "boolean", 145 | "description": "Whether to enable scope multiple mode", 146 | "default": false 147 | }, 148 | "scopeEnumSeparator": { 149 | "type": "string", 150 | "description": "Multiple choice scope separator", 151 | "default": "," 152 | }, 153 | "allowCustomScopes": { 154 | "type": "boolean", 155 | "description": "Whether to show \"custom\" when selecting scopes", 156 | "default": true 157 | }, 158 | "allowEmptyScopes": { 159 | "type": "boolean", 160 | "description": "Whether to show \"empty\" when selecting scopes", 161 | "default": true 162 | }, 163 | "customScopesAlign": { 164 | "type": "string", 165 | "enum": [ 166 | "top", 167 | "bottom", 168 | "top-bottom", 169 | "bottom-top" 170 | ], 171 | "description": "Set the location of empty option (empty) and custom option (custom) in selection range", 172 | "default": "bottom" 173 | }, 174 | "customScopesAlias": { 175 | "type": "string", 176 | "default": "custom" 177 | }, 178 | "emptyScopesAlias": { 179 | "type": "string", 180 | "default": "empty" 181 | }, 182 | "upperCaseSubject": { 183 | "type": "boolean", 184 | "description": "Subject is need upper case first.", 185 | "default": false 186 | }, 187 | "markBreakingChangeMode": { 188 | "type": "boolean", 189 | "description": "Whether to add extra prompt BREAKCHANGE ask. to add an extra \"!\" to the header", 190 | "default": false 191 | }, 192 | "allowBreakingChanges": { 193 | "type": "array", 194 | "items": { 195 | "type": "string" 196 | }, 197 | "description": "Allow breaking changes in the included types output box", 198 | "default": [ 199 | "feat", 200 | "fix" 201 | ] 202 | }, 203 | "breaklineNumber": { 204 | "type": "number", 205 | "description": "set body and BREAKING CHANGE max length to break-line", 206 | "default": 100 207 | }, 208 | "breaklineChar": { 209 | "type": "string", 210 | "description": "body and BREAKINGCHANGES new line char", 211 | "default": "|" 212 | }, 213 | "issuePrefixes": { 214 | "type": "array", 215 | "items": { 216 | "$ref": "#/definitions/Option" 217 | }, 218 | "description": "Provides a select issue prefix box in footer", 219 | "default": "issuePrefixes: [{ value: \"closed\", name: \"ISSUES has been processed\" }]" 220 | }, 221 | "customIssuePrefixAlign": { 222 | "type": "string", 223 | "enum": [ 224 | "top", 225 | "bottom", 226 | "top-bottom", 227 | "bottom-top" 228 | ], 229 | "default": "top" 230 | }, 231 | "emptyIssuePrefixAlias": { 232 | "type": "string", 233 | "default": "skip" 234 | }, 235 | "customIssuePrefixAlias": { 236 | "type": "string", 237 | "default": "custom" 238 | }, 239 | "allowCustomIssuePrefix": { 240 | "type": "boolean", 241 | "description": "Whether to show \"custom\" selecting issue prefixes", 242 | "default": true 243 | }, 244 | "allowEmptyIssuePrefix": { 245 | "type": "boolean", 246 | "description": "Whether to show \"skip(empty)\" when selecting issue prefixes", 247 | "default": true 248 | }, 249 | "confirmColorize": { 250 | "type": "boolean", 251 | "description": "Prompt final determination whether to display the color", 252 | "default": true 253 | }, 254 | "skipQuestions": { 255 | "type": "array", 256 | "items": { 257 | "type": "string", 258 | "enum": [ 259 | "scope", 260 | "body", 261 | "breaking", 262 | "footerPrefix", 263 | "footer", 264 | "confirmCommit" 265 | ] 266 | }, 267 | "description": "List of questions you want to skip", 268 | "examples": [ 269 | [ 270 | "body" 271 | ] 272 | ], 273 | "default": [] 274 | }, 275 | "maxHeaderLength": { 276 | "type": "number", 277 | "description": "Force set max header length | Equivalent setting maxSubjectLength." 278 | }, 279 | "maxSubjectLength": { 280 | "type": "number", 281 | "description": "Force set max subject length." 282 | }, 283 | "isIgnoreCheckMaxSubjectLength": { 284 | "type": "boolean", 285 | "description": "Is not strict subject rule. Just provide prompt word length warning. Effected maxHeader and maxSubject commitlint.", 286 | "default": false 287 | }, 288 | "minSubjectLength": { 289 | "type": "number", 290 | "description": "Force set header width." 291 | }, 292 | "defaultType": { 293 | "type": "string", 294 | "description": "pin type item the top of the types list (match item value)" 295 | }, 296 | "defaultScope": { 297 | "anyOf": [ 298 | { 299 | "type": "string" 300 | }, 301 | { 302 | "type": "array", 303 | "items": { 304 | "type": "string" 305 | } 306 | } 307 | ], 308 | "description": "Whether to use display default value in custom scope" 309 | }, 310 | "defaultSubject": { 311 | "type": "string", 312 | "description": "default value show subject template prompt" 313 | }, 314 | "defaultBody": { 315 | "type": "string", 316 | "description": "default value show body and BREAKINGCHANGES template prompt" 317 | }, 318 | "defaultFooterPrefix": { 319 | "type": "string", 320 | "description": "default value show issuePrefixes custom template prompt" 321 | }, 322 | "defaultIssues": { 323 | "type": "string", 324 | "description": "default value show issue foot template prompt" 325 | }, 326 | "useCommitSignGPG": { 327 | "type": "boolean", 328 | "description": "Whether to use GPG sign commit message (git commit -S -m)", 329 | "default": false 330 | } 331 | }, 332 | "additionalProperties": false 333 | }, 334 | "Answers": { 335 | "type": "object", 336 | "properties": { 337 | "type": { 338 | "type": "string", 339 | "default": "Select the type of change that you're committing:" 340 | }, 341 | "scope": { 342 | "anyOf": [ 343 | { 344 | "type": "string" 345 | }, 346 | { 347 | "type": "array", 348 | "items": { 349 | "type": "string" 350 | } 351 | } 352 | ], 353 | "default": "Denote the SCOPE of this change (optional):" 354 | }, 355 | "customScope": { 356 | "type": "string", 357 | "default": "Denote the SCOPE of this change:" 358 | }, 359 | "subject": { 360 | "type": "string", 361 | "default": "Write a SHORT, IMPERATIVE tense description of the change:\n" 362 | }, 363 | "body": { 364 | "type": "string", 365 | "default": "a LONGER description of the change (optional). Use \"|\" to break new line:\n" 366 | }, 367 | "markBreaking": { 368 | "type": [ 369 | "string", 370 | "boolean" 371 | ], 372 | "default": "Is any BREAKING CHANGE (add \"!\" in header) (optional) ?" 373 | }, 374 | "breaking": { 375 | "type": "string", 376 | "default": "List any BREAKING CHANGES (optional). Use \"|\" to break new line:\n" 377 | }, 378 | "footerPrefixesSelect": { 379 | "type": "string", 380 | "default": "Select the ISSUES type of change (optional):" 381 | }, 382 | "customFooterPrefix": { 383 | "type": "string", 384 | "default": "Input ISSUES prefix:" 385 | }, 386 | "footer": { 387 | "type": "string", 388 | "default": "List any ISSUES AFFECTED by this change. E.g.: #31, #34:" 389 | }, 390 | "confirmCommit": { 391 | "type": "string", 392 | "default": "Are you sure you want to proceed with the commit above?" 393 | }, 394 | "generatingByAI": { 395 | "type": "string", 396 | "default": "Generating your AI commit subject..." 397 | }, 398 | "generatedSelectByAI": { 399 | "type": "string", 400 | "default": "Select suitable subject by AI generated:" 401 | } 402 | }, 403 | "additionalProperties": false 404 | }, 405 | "TypesOption": { 406 | "type": "object", 407 | "properties": { 408 | "name": { 409 | "type": "string", 410 | "description": ": show prompt name" 411 | }, 412 | "value": { 413 | "type": "string", 414 | "description": ": output real value" 415 | }, 416 | "emoji": { 417 | "type": "string", 418 | "description": ": Submit emoji commit string" 419 | } 420 | }, 421 | "additionalProperties": false, 422 | "required": [ 423 | "name", 424 | "value" 425 | ] 426 | }, 427 | "ScopesType": { 428 | "anyOf": [ 429 | { 430 | "type": "array", 431 | "items": { 432 | "type": "string" 433 | } 434 | }, 435 | { 436 | "type": "array", 437 | "items": { 438 | "type": "object", 439 | "properties": { 440 | "name": { 441 | "type": "string" 442 | }, 443 | "value": { 444 | "type": "string" 445 | } 446 | }, 447 | "required": [ 448 | "name" 449 | ], 450 | "additionalProperties": false 451 | } 452 | } 453 | ] 454 | }, 455 | "Option": { 456 | "type": "object", 457 | "properties": { 458 | "name": { 459 | "type": "string", 460 | "description": ": show prompt name" 461 | }, 462 | "value": { 463 | "type": "string", 464 | "description": ": output real value" 465 | } 466 | }, 467 | "required": [ 468 | "name", 469 | "value" 470 | ], 471 | "additionalProperties": false 472 | } 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | 6 | > [!TIP] 7 | > uni-app 分包优化插件化实现 8 | > 9 | > 前往 查看本项目立项背景。 10 | > 11 | > 前往 查看本插件详细发展过程与提交记录。 12 | 13 | ### 🎏 功能与支持 14 | 15 | > !暂时没有对App平台做兼容性实现 16 | > 17 | > 适用于 Uniapp - CLI 或 HBuilderX 创建的 Vue3 项目 18 | 19 | - 分包优化 20 | - 模块异步跨包调用 21 | - 组件异步跨包引用 22 | 23 | ### 📦 安装 24 | 25 | ```bash 26 | pnpm add -D @uni-ku/bundle-optimizer 27 | ``` 28 | 29 | ### 🚀 使用 30 | 31 | #### 0. 插件可配置参数 32 | 33 | > !以下各参数均为可选参数,默认开启所有插件功能,并在项目根目录下生成`async-import.d.ts`与`async-component.d.ts`文件 34 | 35 | |参数-[enable]|类型|默认值|描述| 36 | |---|---|---|---| 37 | |enable|`boolean`\|`object`|`true`|插件功能总开关,`object`时可详细配置各插件启闭状态,详见下列| 38 | |enable.optimization|`boolean`|`true`|分包优化启闭状态| 39 | |enable['async-import']|`boolean`|`true`|模块异步跨包调用启闭状态| 40 | |enable['async-component']|`boolean`|`true`|组件异步跨包引用启闭状态| 41 | 42 | |参数-[dts]|类型|默认值|描述| 43 | |---|---|---|---| 44 | |dts|`boolean`\|`object`|`true`|dts文件输出总配置,`true`时按照下列各配置的默认参数来(根目录下生成`async-import.d.ts`与`async-component.d.ts`文件),`object`时可详细配置各类型文件的生成,详见下列| 45 | |dts.enable|`boolean`|`true`|总配置,是否生成dts文件| 46 | |dts.base|`string`|`./`|总配置,dts文件输出目录,可相对路径,也可绝对路径| 47 | |dts['async-import']|`boolean`\|`object`|`true`|`async-import`dts文件配置,默认为`true`(在项目根目录生成`async-import.d.ts`文件),`object`时可详细配置该项的生成| 48 | |dts['async-import'].enable|`boolean`|`true`|是否生成dts文件| 49 | |dts['async-import'].base|`string`|`./`|dts文件输出目录,可相对路径,也可绝对路径| 50 | |dts['async-import'].name|`string`|`async-import.d.ts`|dts文件名称,需要包含文件后缀| 51 | |dts['async-import'].path|`string`|`${base}/${name}`|dts文件输出路径,如果没有定义此项则会是`${base}/${name}`,否则此配置项优先级更高,可相对路径,也可绝对路径| 52 | |dts['async-component']|`boolean`\|`object`|`true`|`async-component`dts文件配置,默认为`true`(在项目根目录生成`async-component.d.ts`文件),`object`时可详细配置该项的生成| 53 | |dts['async-component'].enable|`boolean`|`true`|是否生成dts文件| 54 | |dts['async-component'].base|`string`|`./`|dts文件输出目录,可相对路径,也可绝对路径| 55 | |dts['async-component'].name|`string`|`async-component.d.ts`|dts文件名称,需要包含文件后缀| 56 | |dts['async-component'].path|`string`|`${base}/${name}`|dts文件输出路径,如果没有定义此项则会是`${base}/${name}`,否则此配置项优先级更高,可相对路径,也可绝对路径| 57 | 58 | |参数-[logger]|类型|默认值|描述| 59 | |---|---|---|---| 60 | |logger|`boolean`\|`string[]`|`false`|插件日志输出总配置,`true`时启用所有子插件的日志功能;`string[]`时可具体启用部分插件的日志,可以是`optimization`、`async-component`、`async-import`| 61 | 62 | #### 1. 引入 `@uni-ku/bundle-optimizer` 63 | 64 | - CLI: `直接编写` 根目录下的 vite.config.* 65 | - HBuilderX: 需要根据你所使用语言, 在根目录下 `创建` vite.config.* 66 | 67 | ##### 简单配置: 68 | 69 | ```js 70 | // vite.config.* 71 | import Uni from '@dcloudio/vite-plugin-uni' 72 | import Optimization from '@uni-ku/bundle-optimizer' 73 | import { defineConfig } from 'vite' 74 | 75 | export default defineConfig({ 76 | plugins: [ 77 | Uni(), 78 | Optimization({ 79 | enable: { 80 | 'optimization': true, 81 | 'async-import': true, 82 | 'async-component': true, 83 | }, 84 | dts: { 85 | enable: true, 86 | base: './', 87 | }, 88 | logger: true, 89 | }), 90 | ], 91 | }) 92 | ``` 93 | 94 | ##### 详细配置说明 95 | 96 | ```js 97 | // vite.config.* 98 | import Uni from '@dcloudio/vite-plugin-uni' 99 | import Optimization from '@uni-ku/bundle-optimizer' 100 | import { defineConfig } from 'vite' 101 | 102 | export default defineConfig({ 103 | plugins: [ 104 | Uni(), 105 | // 可以无需传递任何参数,默认开启所有插件功能,并在项目根目录生成类型定义文件 106 | Optimization({ 107 | // 插件功能开关,默认为true,即开启所有功能 108 | enable: { 109 | 'optimization': true, 110 | 'async-import': true, 111 | 'async-component': true, 112 | }, 113 | // dts文件输出配置,默认为true,即在项目根目录生成类型定义文件 114 | dts: { 115 | 'enable': true, 116 | 'base': './', 117 | // 上面是对类型生成的比较全局的一个配置 118 | // 下面是对每个类型生成的配置,以下各配置均为可选参数 119 | 'async-import': { 120 | enable: true, 121 | base: './', 122 | name: 'async-import.d.ts', 123 | path: './async-import.d.ts', 124 | }, 125 | 'async-component': { 126 | enable: true, 127 | base: './', 128 | name: 'async-component.d.ts', 129 | path: './async-component.d.ts', 130 | }, 131 | }, 132 | // 也可以传递具体的子插件的字符串列表,如 ['optimization', 'async-import', 'async-component'],开启部分插件的log功能 133 | logger: true, 134 | }), 135 | ], 136 | }) 137 | ``` 138 | 139 | #### 2. 修改 `manifest.json` 140 | 141 | 需要修改 manifest.json 中的 `mp-weixin.optimization.subPackages` 配置项为 true,开启方法与vue2版本的uniapp一致。 142 | 143 | ```json 144 | { 145 | "mp-weixin": { 146 | "optimization": { 147 | "subPackages": true 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | > 使用了 `@uni-helper/vite-plugin-uni-manifest` 的项目,修改 `manifest.config.ts` 的对应配置项即可。 154 | 155 | #### 3. 将插件生成的类型标注文件加入 `tsconfig.json` 156 | 157 | 插件运行时默认会在项目根目录下生成 `async-import.d.ts` 与 `async-component.d.ts` 两个类型标注文件,需要将其加入到 `tsconfig.json` 的 `include` 配置项中;如果有自定义dts生成路径,则根据实际情况填写。 158 | 159 | 当然,如果原来的配置已经覆盖到了这两个文件,就可以不加;如果没有运行项目的时候,这两个文件不会生成。 160 | 161 | ```json 162 | { 163 | "include": [ 164 | "async-import.d.ts", 165 | "async-component.d.ts" 166 | ] 167 | } 168 | ``` 169 | 170 | - `async-import.d.ts`:定义了 `AsyncImport` 这个异步函数,用于异步引入模块。 171 | - `async-component.d.ts`:拓展了 `import` 的 `静态引入`,引入路径后面加上`?async`即可实现小程序端的组件异步引用。 172 | - **详见 ** 173 | 174 | > 这两个类型文件不会对项目的运行产生任何影响,只是为了让编辑器能够正确的识别本插件定义的自定义语法、类型。 175 | > 176 | > 这两个文件可以加入到 `.gitignore` 中,不需要提交到代码仓库。 177 | 178 | ### ✨ 例子 179 | 180 | > 以下例子均以CLI创建项目为例, HBuilderX 项目与以上设置同理 ~~, 只要注意是否需要包含 src目录 即可~~。 181 | > 182 | > 现在已经支持 hbx 创建的 vue3 + vite、不以 src 为主要代码目录的项目。 183 | 184 | 🔗 [查看以下例子的完整项目](./examples) 185 | 186 |
187 | 188 | 1. (点击展开) 分包优化 189 | 190 |
191 | 192 | `分包优化` 是本插件运行时默认开启的功能,无需额外配置,只需要确认 `manifest.json` 中的 `mp-weixin.optimization.subPackages` 配置项为 true 即可。 193 | 194 | 详情见本文档中的 [`使用`](#-使用) 部分。 195 | 196 |
197 | 198 |
199 | 200 | 2. (点击展开) 模块异步跨包调用 201 | 202 |
203 | 204 | - `模块异步跨包调用` 是指在一个分包中引用另一个分包中的模块(不限主包与分包),这里的模块可以是 js/ts 模块(插件)、vue 文件。当然,引入 vue 文件一般是没有什么意义的,但是也做了兼容处理。 205 | - `TODO:` 是否支持 json 文件? 206 | 207 | 可以使用函数 `AsyncImport` 这个异步函数来实现模块的异步引入。 208 | 209 | ```js 210 | // js/ts 模块(插件) 异步引入 211 | await AsyncImport('@/pages-sub-async/async-plugin/index').then((res) => { 212 | console.log(res?.AsyncPlugin()) // 该插件导出了一个具名函数 213 | }) 214 | 215 | // vue 文件 异步引入(页面文件) 216 | AsyncImport('@/pages-sub-async/index.vue').then((res) => { 217 | console.log(res.default || res) 218 | }) 219 | 220 | // vue 文件 异步引入(组件文件) 221 | AsyncImport('@/pages-sub-async/async-component/index.vue').then((res) => { 222 | console.log(res.default || res) 223 | }) 224 | ``` 225 | 226 |
227 | 228 |
229 | 230 | 3. (点击展开) 组件异步跨包引用 231 | 232 |
233 | 234 | - `组件异步跨包引用` 是指在一个分包中引用另一个分包中的组件(不限主包与分包),这里的组件就是 vue 文件;貌似支持把页面文件也作为组件引入。 235 | - 在需要跨包引入的组件路径后面加上 `?async` 即可实现异步引入。 236 | 237 | ```vue 238 | 241 | 242 | 247 | ``` 248 | 249 |
250 | 251 | ### 🏝 周边 252 | 253 | |项目|描述| 254 | |---|---| 255 | |[Uni Ku](https://github.com/uni-ku)|有很多 Uniapp(Uni) 的酷(Ku) 😎| 256 | |[create-uni](https://uni-helper.js.org/create-uni)|🛠️ 快速创建uni-app项目| 257 | |[Wot Design Uni](https://github.com/Moonofweisheng/wot-design-uni/)|一个基于Vue3+TS开发的uni-app组件库,提供70+高质量组件| 258 | 259 | ### 🧔 找到我 260 | 261 | > 加我微信私聊,方便定位、解决问题。 262 | 263 | 264 | 265 | 269 | 270 |
266 | wechat-qrcode 267 |

微信

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

微信

285 |
287 | alipay 288 |

支付宝

289 |
292 | 293 | #### 赞赏榜单 294 | 295 |

296 | 297 | sponsors 298 | 299 |

300 | 301 | --- 302 | 303 |

304 | Happy coding! 305 |

306 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: ['src/index'], 5 | declaration: true, 6 | clean: true, 7 | rollup: { 8 | emitCJS: true, 9 | inlineDependencies: false, 10 | }, 11 | failOnWarn: false, 12 | // 排除隐式外部依赖 13 | externals: [], 14 | }) 15 | -------------------------------------------------------------------------------- /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/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/hbx+vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lodash": "^4.17.21" 4 | }, 5 | "devDependencies": { 6 | "@uni-ku/bundle-optimizer": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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/hbx+vue3/pages-sub/index/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages-sub/plugins/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test(msg) { 3 | console.log(msg, 'plugin:test') 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/hbx+vue3/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | 70 | -------------------------------------------------------------------------------- /examples/hbx+vue3/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uni-ku/bundle-optimizer/ba70175638819891f7bfd918ebf4f75e3a2b5067/examples/hbx+vue3/static/logo.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | }) -------------------------------------------------------------------------------- /examples/vue3+vite+ts/.gitignore: -------------------------------------------------------------------------------- 1 | async-component.d.ts 2 | async-import.d.ts -------------------------------------------------------------------------------- /examples/vue3+vite+ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /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": "3.0.0-4020820240925001", 13 | "@dcloudio/uni-components": "3.0.0-4020820240925001", 14 | "@dcloudio/uni-h5": "3.0.0-4020820240925001", 15 | "@dcloudio/uni-mp-weixin": "3.0.0-4020820240925001", 16 | "@https-enable/colors": "^0.1.1", 17 | "lodash": "^4.17.21", 18 | "vue": "3.4.21" 19 | }, 20 | "devDependencies": { 21 | "@dcloudio/types": "^3.4.10", 22 | "@dcloudio/uni-automator": "3.0.0-4020820240925001", 23 | "@dcloudio/uni-cli-shared": "3.0.0-4020820240925001", 24 | "@dcloudio/vite-plugin-uni": "3.0.0-4020820240925001", 25 | "@https-enable/types": "^0.1.1", 26 | "@types/lodash": "^4.17.13", 27 | "@uni-ku/bundle-optimizer": "workspace:*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-async/component.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-async/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | 53 | 54 | 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-demo/components/demo1.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages-sub-demo/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 57 | 58 | 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/vue3+vite+ts/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { defineConfig } from 'vite' 5 | 6 | export default defineConfig({ 7 | base: './', 8 | plugins: [ 9 | Uni(), 10 | // 可以无需传递任何参数,默认开启所有插件功能,并在项目根目录生成类型定义文件 11 | Optimization({ 12 | enable: { 13 | 'optimization': true, 14 | 'async-import': true, 15 | 'async-component': true, 16 | }, 17 | dts: { 18 | 'enable': true, 19 | 'base': 'src/types', 20 | // 上面是对类型生成的比较全局的一个配置 21 | // 下面是对每个类型生成的配置,以下各配置均为可选参数 22 | 'async-import': { 23 | enable: true, 24 | base: 'src/types', 25 | name: 'async-import.d.ts', 26 | path: 'src/types/async-import.d.ts', 27 | }, 28 | 'async-component': { 29 | enable: true, 30 | base: 'src/types', 31 | name: 'async-component.d.ts', 32 | path: 'src/types/async-component.d.ts', 33 | }, 34 | }, 35 | logger: true, 36 | }), 37 | ], 38 | resolve: { 39 | alias: { 40 | '@': fileURLToPath(new URL('./src', import.meta.url)), 41 | }, 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uni-ku/bundle-optimizer", 3 | "type": "module", 4 | "version": "1.3.6", 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 | }, 65 | "devDependencies": { 66 | "@antfu/eslint-config": "^4.3.0", 67 | "@commitlint/cli": "^19.3.0", 68 | "@commitlint/config-conventional": "^19.2.2", 69 | "@types/node": "^22.10.2", 70 | "bumpp": "^9.9.1", 71 | "commitizen": "^4.3.0", 72 | "cz-git": "^1.9.1", 73 | "eslint": "^9.21.0", 74 | "jiti": "^2.4.2", 75 | "lint-staged": "^15.2.2", 76 | "simple-git-hooks": "^2.11.1", 77 | "typescript": "^5.7.3", 78 | "unbuild": "^2.0.0", 79 | "vite": "^4.0.0" 80 | }, 81 | "simple-git-hooks": { 82 | "pre-commit": "pnpm lint-staged", 83 | "commit-msg": "npx commitlint --edit ${1}" 84 | }, 85 | "lint-staged": { 86 | "*.{js,ts,tsx,vue,md}": [ 87 | "eslint . --fix --flag unstable_ts_config" 88 | ] 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "node_modules/cz-git" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | -------------------------------------------------------------------------------- /src/async-component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncComponentProcessor } from './plugin/async-component-processor' 2 | 3 | export default (...options: Parameters) => { 4 | return [ 5 | // 处理 `.vue?async` 查询参数的静态导入 6 | AsyncComponentProcessor(...options), 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/async-import.ts: -------------------------------------------------------------------------------- 1 | import { AsyncImportProcessor } from './plugin/async-import-processor' 2 | 3 | export default (...options: Parameters) => { 4 | return [ 5 | // 处理 `AsyncImport` 函数调用的路径传参 6 | AsyncImportProcessor(...options), 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/common/AsyncComponents.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | export type BindingAsyncComponents = Record 7 | 8 | export interface TemplateDescriptor { 9 | bindingAsyncComponents: BindingAsyncComponents | null 10 | } 11 | 12 | export class AsyncComponents { 13 | scriptDescriptors: Map = new Map() 14 | jsonAsyncComponentsCache: Map = new Map() 15 | /** 当前状态下热更新时会导致把原有的json内容清除,操作过的page-json需要记录之前的内容 */ 16 | pageJsonCache: Map>> = new Map() 17 | 18 | constructor() {} 19 | 20 | rename = (name: string) => name.startsWith('wx-') ? name.replace('wx-', 'weixin-') : name 21 | 22 | addScriptDescriptor(filename: string, binding: BindingAsyncComponents) { 23 | binding && filename && this.scriptDescriptors.set(filename, { 24 | bindingAsyncComponents: binding, 25 | }) 26 | } 27 | 28 | addAsyncComponents(filename: string, json: BindingAsyncComponents) { 29 | this.jsonAsyncComponentsCache.set(filename, json) 30 | } 31 | 32 | generateBinding(tag: string, path: string) { 33 | return { tag, value: path, type: 'asyncComponent' } as const 34 | } 35 | 36 | getComponentPlaceholder(filename: string) { 37 | const cache = this.jsonAsyncComponentsCache.get(filename) 38 | if (!cache) 39 | return null 40 | 41 | const componentPlaceholder = Object.entries(cache).reduce>((p, [key, value]) => { 42 | p[this.rename(key)] = 'view' 43 | return p 44 | }, {}) 45 | return componentPlaceholder 46 | } 47 | 48 | generateComponentPlaceholderJson(filename: string, originJson: Record = {}) { 49 | const componentPlaceholder = this.getComponentPlaceholder(filename) 50 | return Object.assign(originJson || {}, componentPlaceholder || {}) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 { 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/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 | 19 | constructor(level: LogLevel = LogLevel.INFO, context: string = 'Plugin', isImplicit = false) { 20 | this.level = level 21 | this.context = context 22 | this.isImplicit = isImplicit 23 | } 24 | 25 | private log(level: LogLevel, message: string, isImplicit?: boolean) { 26 | if (this.shouldLog(level)) { 27 | const coloredMessage = this.getColoredMessage(level, message) 28 | if (isImplicit ?? this.isImplicit) { 29 | // TODO: 相关的隐式log,需要通过外部环境变量启用 30 | // 此处暂时不显示 31 | } 32 | else { 33 | const c = 69 34 | const colorCode = `\u001B[3${c < 8 ? c : `8;5;${c}`};1m` 35 | console.log(` ${chalk(`${colorCode}${this.context}`)} ${coloredMessage}`) 36 | } 37 | } 38 | } 39 | 40 | private shouldLog(level: LogLevel): boolean { 41 | const levels: LogLevel[] = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR] 42 | return levels.indexOf(level) >= levels.indexOf(this.level) 43 | } 44 | 45 | private getColoredMessage(level: LogLevel, message: string): string { 46 | switch (level) { 47 | case LogLevel.DEBUG: 48 | return chalk.blue(`[${level}] ${message}`) 49 | case LogLevel.INFO: 50 | return chalk.green(`[${level}] ${message}`) 51 | case LogLevel.WARN: 52 | return chalk.yellow(`[${level}] ${message}`) 53 | case LogLevel.ERROR: 54 | return chalk.red(`[${level}] ${message}`) 55 | default: 56 | return message 57 | } 58 | } 59 | 60 | debug(message: string, isImplicit?: boolean) { 61 | this.log(LogLevel.DEBUG, message, isImplicit) 62 | } 63 | 64 | info(message: string, isImplicit?: boolean) { 65 | this.log(LogLevel.INFO, message, isImplicit) 66 | } 67 | 68 | warn(message: string, isImplicit?: boolean) { 69 | this.log(LogLevel.WARN, message, isImplicit) 70 | } 71 | 72 | error(message: string, isImplicit?: boolean) { 73 | this.log(LogLevel.ERROR, message, isImplicit) 74 | } 75 | } 76 | 77 | export const logger = new Logger(LogLevel.INFO, 'uni-ku:bundle-optimizer') 78 | -------------------------------------------------------------------------------- /src/common/PackageModules.ts: -------------------------------------------------------------------------------- 1 | import type { ManualChunkMeta, ModuleInfo } from '../type' 2 | 3 | // **直接**引入到包内的`模块A`是能被得知并编译到包内的 4 | // 而`模块A`引入的`模块B`是不能被得知的,因为`模块A`是`模块B`的引入者,`模块B`是`模块A`的依赖 5 | // 当下的模式,`模块B`是不能被明确得知是属于哪个包的,所以需要以下工具类来处理这种情况 6 | // 实现链式依赖的查找 7 | export class PackageModules { 8 | /** 9 | * 包的模块信息记录 10 | * 11 | * @description 记录一个包中引入的模块信息 12 | * @description key 为 undefined 时,表示是主包的模块信息 13 | */ 14 | private modulesRecord: Record = {} 15 | 16 | public chunkMeta: null | ManualChunkMeta = null 17 | 18 | moduleIdProcessor: (moduleId: string) => string = id => id 19 | 20 | constructor(moduleIdProcessor?: typeof this.moduleIdProcessor) { 21 | if (moduleIdProcessor && typeof moduleIdProcessor === 'function') 22 | this.moduleIdProcessor = moduleIdProcessor 23 | } 24 | 25 | static isCommonJsModule(moduleInfo: ModuleInfo) { 26 | return !!moduleInfo?.meta?.commonjs?.isCommonJS 27 | } 28 | 29 | /** 30 | * add module record 31 | */ 32 | public addModuleRecord(packageId: string | undefined, moduleInfo: ModuleInfo) { 33 | let moduleId = moduleInfo.id 34 | if (!moduleId) 35 | return 36 | 37 | moduleId = this.moduleIdProcessor(moduleId) 38 | 39 | if (!moduleId) 40 | return 41 | 42 | if (!this.modulesRecord[`${packageId}`]) { 43 | this.modulesRecord[`${packageId}`] = {} 44 | } 45 | 46 | this.modulesRecord[`${packageId}`][moduleId] = { 47 | isMain: packageId === undefined, 48 | id: moduleInfo.id, 49 | meta: moduleInfo.meta, 50 | importers: moduleInfo.importers, 51 | importedIds: moduleInfo.importedIds, 52 | importedIdResolutions: moduleInfo.importedIdResolutions, 53 | dynamicImporters: moduleInfo.dynamicImporters, 54 | dynamicallyImportedIds: moduleInfo.dynamicallyImportedIds, 55 | dynamicallyImportedIdResolutions: moduleInfo.dynamicallyImportedIdResolutions, 56 | } 57 | } 58 | 59 | /** 60 | * clear module record 61 | */ 62 | public clearModuleRecord() { 63 | return this.modulesRecord = {} 64 | } 65 | 66 | /** 67 | * find module record in `importers` 68 | * @param importers 模块引入者列表 | 哪些模块引入了该模块之意 69 | * @param moduleIdProcessor 模块id处理器 - 可选 70 | * @description 查找`引入者列表`中是否有属于`modulesRecord`中的模块,返回命中的模块信息 71 | * @description 这意味着查找`modulesRecord`中的哪些包引入了该模块,说明这个模块是引入这个包内的依赖的链式依赖(依赖的依赖) 72 | */ 73 | public findModuleInImporters(importers: readonly string[], moduleIdProcessor?: typeof this.moduleIdProcessor | null) { 74 | return importers.reduce((pkgs, importerId) => { 75 | for (const packageId in this.modulesRecord) { 76 | const _moduleIdProcessor = moduleIdProcessor || this.moduleIdProcessor 77 | const moduleId = _moduleIdProcessor(importerId) 78 | 79 | if (this.modulesRecord[packageId][moduleId]) { 80 | // 说明这个`包`的某个依赖引入了这个`模块`,记录下来 81 | pkgs[packageId] = this.modulesRecord[packageId] 82 | } 83 | } 84 | return pkgs 85 | }, {} as typeof this.modulesRecord) 86 | } 87 | 88 | /** 89 | * @deprecated 暂时没有使用的场景 90 | */ 91 | public findModuleImported(moduleId: string) { 92 | return Object.entries(this.modulesRecord).reduce((pkgs, [packageId, packageModules]) => { 93 | const tempModule = packageModules[moduleId] 94 | if (tempModule) { 95 | pkgs[packageId] = packageModules 96 | } 97 | return pkgs 98 | }, {} as typeof this.modulesRecord) 99 | } 100 | 101 | /** 102 | * process module 103 | * @description 处理commonjs模块的链式依赖 104 | */ 105 | public processModule(moduleInfo: ModuleInfo): [string | null | undefined, ModuleInfo] | null | undefined { 106 | let resultPackageId: string | null = null 107 | for (const packageId in this.modulesRecord) { 108 | const packageModules = this.modulesRecord[packageId] 109 | for (const moduleId in packageModules) { 110 | const itemInfo = packageModules[moduleId] 111 | if (itemInfo.importedIds?.some(importedId => importedId.includes(moduleInfo.id))) { 112 | resultPackageId = packageId 113 | break 114 | } 115 | } 116 | if (resultPackageId) { 117 | break 118 | } 119 | } 120 | if (resultPackageId) { 121 | this.addModuleRecord(resultPackageId, moduleInfo) 122 | return [resultPackageId, moduleInfo] 123 | } 124 | } 125 | } 126 | 127 | export default PackageModules 128 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | export const EXTNAME_JS_RE = /\.(js|jsx|ts|uts|tsx|mjs)$/ 4 | export const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/ 5 | 6 | export const knownJsSrcRE 7 | = /\.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba|mdx)(?:$|\?)/ 8 | 9 | export const CSS_LANGS_RE 10 | = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ 11 | 12 | /** `assets` 或者 `./assets` 开头的文件夹 */ 13 | export const ASSETS_DIR_RE = /^(\.?\/)?assets\// 14 | 15 | /** `src` 或者 `./src` 开头的文件夹 */ 16 | export const SRC_DIR_RE = /^(\.?\/)?src\// 17 | 18 | /** 文件后缀 */ 19 | export const EXT_RE = /\.\w+$/ 20 | 21 | export function isCSSRequest(request: string): boolean { 22 | return CSS_LANGS_RE.test(request) 23 | } 24 | 25 | /** 26 | * 项目根路径 27 | * 28 | * // TODO: 后续自实现项目根路径的查找 29 | */ 30 | export const ROOT_DIR = process.env.VITE_ROOT_DIR! 31 | if (!ROOT_DIR) { 32 | throw new Error('`ROOT_DIR` is not defined') 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite' 2 | import type { IOptions } from './type' 3 | import AsyncComponent from './async-component' 4 | import AsyncImport from './async-import' 5 | import { ParseOptions } from './common/ParseOptions' 6 | import UniappSubPackagesOptimization from './main' 7 | import { initializeVitePathResolver } from './utils' 8 | 9 | export default (options: IOptions = {}): PluginOption => { 10 | const parse = new ParseOptions(options) 11 | 12 | return [ 13 | { 14 | name: 'optimization:initialized', 15 | config(config) { 16 | initializeVitePathResolver(config) 17 | }, 18 | }, 19 | // 分包优化 20 | parse.enable.optimization && UniappSubPackagesOptimization(parse.logger.optimization), 21 | // js/ts插件的异步调用 22 | parse.enable['async-import'] && AsyncImport(parse.dts['async-import'], parse.logger['async-import']), 23 | // vue组件的异步调用 24 | parse.enable['async-component'] && AsyncComponent(parse.dts['async-component'], parse.logger['async-component']), 25 | ] 26 | } 27 | 28 | export type * from './type' 29 | -------------------------------------------------------------------------------- /src/main.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 { EXTNAME_JS_RE, ROOT_DIR } from './constants' 11 | import { moduleIdProcessor as _moduleIdProcessor, normalizePath } from './utils' 12 | 13 | /** 14 | * uniapp 分包优化插件 15 | */ 16 | export function UniappSubPackagesOptimization(enableLogger: boolean): Plugin { 17 | const platform = process.env.UNI_PLATFORM 18 | const inputDir = process.env.UNI_INPUT_DIR 19 | 20 | if (!platform || !inputDir) { 21 | throw new Error('`UNI_INPUT_DIR` or `UNI_PLATFORM` is not defined') 22 | } 23 | 24 | // #region 分包优化参数获取 25 | const manifestJson = parseManifestJsonOnce(inputDir) 26 | const platformOptions = manifestJson[platform] || {} 27 | const optimization = platformOptions.optimization || {} 28 | process.env.UNI_OPT_TRACE = `${!!optimization.subPackages}` 29 | 30 | const pagesJsonPath = path.resolve(inputDir, 'pages.json') 31 | const jsonStr = fs.readFileSync(pagesJsonPath, 'utf8') 32 | const { appJson } = parseMiniProgramPagesJson(jsonStr, platform, { subpackages: true }) 33 | process.UNI_SUBPACKAGES = appJson.subPackages || {} 34 | // #endregion 35 | 36 | // #region subpackage 37 | const UNI_SUBPACKAGES = process.UNI_SUBPACKAGES || {} 38 | const subPkgsInfo: ISubPkgsInfo[] = Object.values(UNI_SUBPACKAGES) 39 | const normalFilter = ({ independent }: ISubPkgsInfo) => !independent 40 | const independentFilter = ({ independent }: ISubPkgsInfo) => independent 41 | /** 先去除尾部的`/`,再添加`/`,兼容pages.json中以`/`结尾的路径 */ 42 | const map2Root = ({ root }: ISubPkgsInfo) => `${root.replace(/\/$/, '')}/` 43 | const subPackageRoots = subPkgsInfo.map(map2Root) 44 | const normalSubPackageRoots = subPkgsInfo.filter(normalFilter).map(map2Root) 45 | const independentSubpackageRoots = subPkgsInfo.filter(independentFilter).map(map2Root) 46 | 47 | /** 48 | * # id处理器 49 | * @description 将id中的moduleId转换为相对于inputDir的路径并去除查询参数后缀 50 | */ 51 | function moduleIdProcessor(id: string) { 52 | return _moduleIdProcessor(id, process.env.UNI_INPUT_DIR) 53 | } 54 | /** 55 | * 判断该文件模块的来源 56 | */ 57 | const moduleFrom = (id: string): 58 | { from: 'main' | 'node_modules', clearId: string } 59 | | { from: 'sub', clearId: string, pkgRoot: string } 60 | | undefined => { 61 | let root = normalizePath(ROOT_DIR) 62 | if (!root.endsWith('/')) 63 | root = `${root}/` 64 | 65 | const clearId = moduleIdProcessor(id) 66 | 67 | if (!path.isAbsolute(clearId)) { 68 | const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(clearId).indexOf(root) === 0) 69 | if (pkgRoot === undefined) 70 | return { from: clearId.startsWith('node_modules/') ? 'node_modules' : 'main', clearId } 71 | else 72 | return { from: 'sub', clearId, pkgRoot } 73 | } 74 | else { 75 | // clearId.startsWith(root) && TODO: 放宽条件,兼容 workspace 项目 76 | if (clearId.includes('/node_modules/')) 77 | return { from: 'node_modules', clearId } 78 | } 79 | } 80 | 81 | /** 查找模块列表中是否有属于子包的模块 */ 82 | const findSubPackages = function (importers: readonly string[]) { 83 | return importers.reduce((pkgs, item) => { 84 | const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(item).indexOf(root) === 0) 85 | pkgRoot && pkgs.add(pkgRoot) 86 | return pkgs 87 | }, new Set()) 88 | } 89 | 90 | /** 判断是否有非子包的import (是否被非子包引用) */ 91 | const hasNoSubPackage = function (importers: readonly string[]) { 92 | return importers.some((item) => { 93 | // 遍历所有的子包根路径,如果模块的路径不包含子包路径,就说明被非子包引用了 94 | return !subPackageRoots.some(root => moduleIdProcessor(item).indexOf(root) === 0) 95 | }) 96 | } 97 | /** 判断是否有来自`node_modules`下的依赖 */ 98 | const hasNodeModules = function (importers: readonly string[]) { 99 | return hasNoSubPackage(importers) && importers.some((item) => { 100 | return moduleIdProcessor(item).includes('node_modules') 101 | }) 102 | } 103 | /** 查找来自 主包 下的依赖 */ 104 | const findMainPackage = function (importers: readonly string[]) { 105 | const list = importers.filter((item) => { 106 | const id = moduleIdProcessor(item) 107 | // 排除掉子包和第三方包之后,剩余的视为主包 108 | return !subPackageRoots.some(root => id.indexOf(root) === 0) && !id.includes('node_modules') 109 | }) 110 | return list 111 | } 112 | /** 查找来自 主包 下的组件 */ 113 | const findMainPackageComponent = function (importers: readonly string[]) { 114 | const list = findMainPackage(importers) 115 | const mainPackageComponent = new Set(list 116 | .map(item => moduleIdProcessor(item)) 117 | .filter(name => name.endsWith('.vue') || name.endsWith('.nvue'))) 118 | return mainPackageComponent 119 | } 120 | /** 判断是否含有项目入口文件的依赖 */ 121 | const hasEntryFile = function (importers: readonly string[], meta: ManualChunkMeta) { 122 | const list = findMainPackage(importers) 123 | return list.some(item => meta.getModuleInfo(item)?.isEntry) 124 | } 125 | /** 判断该模块引用的模块是否有跨包引用的组件 */ 126 | const hasMainPackageComponent = function (moduleInfo: Partial, subPackageRoot?: string) { 127 | if (moduleInfo.id && moduleInfo.importedIdResolutions) { 128 | for (let index = 0; index < moduleInfo.importedIdResolutions.length; index++) { 129 | const m = moduleInfo.importedIdResolutions[index] 130 | 131 | if (m && m.id) { 132 | const name = moduleIdProcessor(m.id) 133 | // 判断是否为组件 134 | if (name.includes('.vue') || name.includes('.nvue')) { 135 | // 判断存在跨包引用的情况(该组件的引用路径不包含子包路径,就说明跨包引用了) 136 | if (subPackageRoot && !name.includes(subPackageRoot)) { 137 | if (process.env.UNI_OPT_TRACE) { 138 | console.log('move module to main chunk:', moduleInfo.id, 'from', subPackageRoot, 'for component in main package:', name) 139 | } 140 | 141 | // 独立分包除外 142 | const independentRoot = independentSubpackageRoots.find(root => name.includes(root)) 143 | if (!independentRoot) { 144 | return true 145 | } 146 | } 147 | } 148 | else { 149 | return hasMainPackageComponent(m, subPackageRoot) 150 | } 151 | } 152 | } 153 | } 154 | return false 155 | } 156 | // #endregion 157 | 158 | logger.info('[optimization] 分包优化插件已启用', !enableLogger) 159 | 160 | return { 161 | name: 'uniapp-subpackages-optimization', 162 | enforce: 'post', // 控制执行顺序,post 保证在其他插件之后执行 163 | config(config, { command }) { 164 | if (!platform.startsWith('mp')) { 165 | logger.warn('[optimization] 分包优化插件仅需在小程序平台启用,跳过', !enableLogger) 166 | return 167 | } 168 | 169 | const UNI_OPT_TRACE = process.env.UNI_OPT_TRACE === 'true' 170 | logger.info(`[optimization] 分包优化开启状态: ${UNI_OPT_TRACE}`, !true) // !!! 此处始终开启log 171 | if (!UNI_OPT_TRACE) 172 | return 173 | 174 | const originalOutput = config?.build?.rollupOptions?.output 175 | 176 | const existingManualChunks 177 | = (Array.isArray(originalOutput) ? originalOutput[0]?.manualChunks : originalOutput?.manualChunks) as ManualChunksOption 178 | 179 | // 合并已有的 manualChunks 配置 180 | const mergedManualChunks: ManualChunksOption = (id, meta) => { 181 | /** 依赖图谱分析 */ 182 | function getDependencyGraph(startId: string, getRelated: (info: ModuleInfo) => readonly string[] = info => info.importers): string[] { 183 | const visited = new Set() 184 | const result: string[] = [] 185 | 186 | // 支持自定义遍历方向的泛化实现 187 | function traverse( 188 | currentId: string, 189 | getRelated: (info: ModuleInfo) => readonly string[], // 控制遍历方向的回调函数 190 | ) { 191 | if (visited.has(currentId)) 192 | return 193 | 194 | visited.add(currentId) 195 | result.push(currentId) 196 | 197 | const moduleInfo = meta.getModuleInfo(currentId) 198 | if (!moduleInfo) 199 | return 200 | 201 | getRelated(moduleInfo).forEach((relatedId) => { 202 | traverse(relatedId, getRelated) 203 | }) 204 | } 205 | 206 | // 示例:向上追踪 importers(谁导入了当前模块) 207 | traverse(startId, getRelated) 208 | 209 | // 若需要向下追踪 dependencies(当前模块导入了谁): 210 | // traverse(startId, (info) => info.dependencies); 211 | 212 | return result 213 | } 214 | 215 | const normalizedId = normalizePath(id) 216 | const filename = normalizedId.split('?')[0] 217 | 218 | // #region ⚠️ 以下代码是分包优化的核心逻辑 219 | // 处理项目内的js,ts文件 220 | if (EXTNAME_JS_RE.test(filename) && (!filename.startsWith(inputDir) || filename.includes('node_modules'))) { 221 | // 如果这个资源只属于一个子包,并且其调用组件的不存在跨包调用的情况,那么这个模块就会被加入到对应的子包中。 222 | const moduleInfo = meta.getModuleInfo(id) 223 | if (!moduleInfo) { 224 | throw new Error(`moduleInfo is not found: ${id}`) 225 | } 226 | const importers = moduleInfo.importers || [] // 依赖当前模块的模块id 227 | const matchSubPackages = findSubPackages(importers) 228 | // 查找直接引用关系中是否有主包的组件文件模块 229 | const mainPackageComponent = findMainPackageComponent(importers) 230 | // 是否有被项目入口文件直接引用 231 | const isEntry = hasEntryFile(importers, meta) 232 | 233 | const moduleFromInfos = moduleFrom(id) 234 | 235 | let isMain = false 236 | if ( 237 | // 未知来源的模块、commonjsHelpers => 打入主包 238 | (!moduleFromInfos || moduleFromInfos.clearId === 'commonjsHelpers.js') 239 | // 被入口文件直接引用的 => 打入主包 240 | || isEntry 241 | // 主包未被引用的模块 => 打入主包(要么是项目主入口文件、要么就是存在隐式引用) 242 | // 主包没有匹配到子包的引用 => 打入主包(只被主包引用) 243 | || (moduleFromInfos.from === 'main' && (!importers.length || !matchSubPackages.size)) 244 | // 直系(浅层)依赖判断:匹配到存在主包组件的引用 245 | || mainPackageComponent.size > 0 246 | // 直系(浅层)依赖判断:匹配到多个子包的引用 => 打入主包 247 | || matchSubPackages.size > 1 248 | ) { 249 | // 这里使用 flag 控制,而不能使用 return 250 | // 直接 return 和 return "common/vendor" 都是不对的 251 | // 直接放空,让后续的插件自行抉择 252 | isMain = true 253 | } 254 | 255 | if (!isMain) { 256 | // 直系(浅层)判断 => 打入子包(必须判断是否有没有非子包的引用的模块,因为暂时无法判断第三方的模块的依赖链的情况) 257 | if (matchSubPackages.size === 1 && !hasNoSubPackage(importers)) { 258 | return `${matchSubPackages.values().next().value}common/vendor` 259 | } 260 | 261 | // 搜寻引用图谱 262 | const importersGraph = getDependencyGraph(id) 263 | const newMatchSubPackages = findSubPackages(importersGraph) 264 | // 查找引用图谱中是否有主包的组件文件模块 265 | const newMainPackageComponent = findMainPackageComponent(importersGraph) 266 | 267 | // 引用图谱中只找到一个子包的引用,并且没有出现主包的组件,则说明只归属该子包 268 | if (newMatchSubPackages.size === 1 && newMainPackageComponent.size === 0) { 269 | return `${newMatchSubPackages.values().next().value}common/vendor` 270 | } 271 | } 272 | } 273 | 274 | // #endregion 275 | 276 | // 调用已有的 manualChunks 配置 | 此处必须考虑到原有的配置,是为了使 uniapp 原本的分包配置生效 277 | if (existingManualChunks && typeof existingManualChunks === 'function') 278 | return existingManualChunks(id, meta) 279 | } 280 | 281 | return { 282 | build: { 283 | rollupOptions: { 284 | output: { 285 | manualChunks: mergedManualChunks, 286 | }, 287 | }, 288 | }, 289 | } 290 | }, 291 | } 292 | } 293 | 294 | export default UniappSubPackagesOptimization 295 | -------------------------------------------------------------------------------- /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 | /** 17 | * 处理 `import xxx from "*.vue?async"` 形式的调用 18 | * @description `transform`阶段处理识别以上形式的导入语句,做相关的缓存处理;并将`?async`查询参数去除,避免后续编译处理识别不来该语句 19 | * @description `generateBundle`阶段处理生成相关页面的 page-json 文件,注入`componentPlaceholder`配置 20 | */ 21 | export function AsyncComponentProcessor(options: DtsType, enableLogger: boolean): Plugin { 22 | const inputDir = process.env.UNI_INPUT_DIR 23 | const platform = process.env.UNI_PLATFORM 24 | const AsyncComponentsInstance = new AsyncComponents() 25 | 26 | const isMP = platform?.startsWith('mp-') 27 | 28 | /** 生成类型定义文件 */ 29 | function generateTypeFile(parseResult?: ReturnType) { 30 | if (options === false || options.enable === false) 31 | return 32 | 33 | const typesFilePath = path.resolve(ROOT_DIR, normalizePath(options.path)) 34 | ensureDirectoryExists(typesFilePath) 35 | let cache: string[] = [] // 缓存已经生成的类型定义,防止开发阶段热更新时部分类型定义生成丢失 36 | if (fs.existsSync(typesFilePath)) { 37 | const list = lexFunctionCalls(fs.readFileSync(typesFilePath, 'utf-8'), 'import').flatMap(({ args }) => args.map(({ value }) => value.toString())) 38 | list && list.length && (cache = Array.from(new Set(list))) 39 | } 40 | const typeDefinition = generateModuleDeclaration(parseResult, cache) 41 | fs.writeFileSync(typesFilePath, typeDefinition) 42 | logger.info(`[async-component] ${parseResult === undefined ? '初始化' : '生成'}类型定义文件 ${typesFilePath.replace(`${ROOT_DIR}\\`, '')}`, !enableLogger) 43 | } 44 | generateTypeFile() // 初始化类型定义文件 45 | 46 | logger.info('[async-component] 异步组件处理器已启用', !enableLogger) 47 | 48 | return { 49 | name: 'async-component-processor', 50 | async transform(source, importer) { 51 | // 热更新时,由于含有 async 查询参数的导入语句会删除查询部分(为的是避免后续编译处理识别不来该语句) 52 | // 所以热更新代码时,已经被处理过的代码再次处理时,原本应该被处理的相关查询参数代码已经被删除了,将不会再处理该代码文件 53 | // TODO: 后续需要针对以上问题进行优化((好像解决了?) 54 | const parseResult = lexDefaultImportWithQuery(source).filter(({ modulePath }) => modulePath.value.toString().split('?')[0].endsWith('.vue')) 55 | 56 | if (!importer.split('?')[0].endsWith('.vue') || parseResult.length === 0 || !parseResult.some(({ query }) => query.some(({ value }) => value.toString().trim() === 'async'))) { 57 | return 58 | } 59 | 60 | // 生成类型定义文件 61 | generateTypeFile(parseResult) 62 | 63 | const filename = removeExt(normalizeMiniProgramFilename(importer, inputDir)) 64 | 65 | const tempBindings: TemplateDescriptor['bindingAsyncComponents'] = {} 66 | 67 | const magicString = new MagicString(source) 68 | parseResult.forEach(({ full, fullPath, defaultVariable, modulePath, query }) => { 69 | const cache: Record = {} 70 | query.forEach(({ start, end, value }, index, list) => { 71 | const prevChar = source[start - 1] 72 | 73 | if (['async', ''].includes(value.toString().trim()) && (start !== end)) { 74 | magicString.overwrite(start, end, '') 75 | 76 | if (prevChar === '&') { 77 | magicString.overwrite(start - 1, start, '') 78 | } 79 | cache[index] = { start, end, value } 80 | 81 | // ---- 记录异步组件 [小程序环境下] ---- 82 | if (isMP) { 83 | const url = modulePath.value.toString() 84 | let normalizedPath = getVitePathResolver()(url, true) 85 | // 根据调用主从关系,获取引用文件的相对路径 86 | normalizedPath = calculateRelativePath(importer, normalizedPath) 87 | // 去除 .vue 后缀 88 | normalizedPath = normalizedPath.replace(/\.vue$/, '') 89 | const tag = kebabCase(defaultVariable.value.toString()) 90 | tempBindings[tag] = AsyncComponentsInstance.generateBinding(tag, normalizedPath) 91 | } 92 | // ---- 记录异步组件 | 其他步骤是全平台的都要的,因为在 transform 阶段需要把 `import xxx from "*.vue?async"` 查询参数去除,否则会影响后续编译 ---- 93 | } 94 | }) 95 | 96 | if (cache[0]) { 97 | // 查找第一个不连续的数字之前的数字 98 | const flag = findFirstNonConsecutiveBefore(Object.keys(cache).map(Number)) 99 | 100 | const { start, end } = flag !== null ? query[flag + 1] : cache[0] 101 | const char = flag !== null ? '&' : '?' 102 | const prevChar = source[start - 1] 103 | if (prevChar === char) { 104 | magicString.overwrite(start - 1, start, '') 105 | } 106 | } 107 | }) 108 | 109 | // ---- 异步组件数据加入缓存 [小程序环境下] ---- 110 | if (isMP) { 111 | AsyncComponentsInstance.addScriptDescriptor(filename, tempBindings) 112 | AsyncComponentsInstance.addAsyncComponents(filename, tempBindings) 113 | } 114 | // ---- 异步组件数据加入缓存 ---- 115 | 116 | return { 117 | code: magicString.toString(), 118 | map: magicString.generateMap({ hires: true }), 119 | } 120 | }, 121 | generateBundle(_, bundle) { 122 | if (!isMP) 123 | return 124 | 125 | AsyncComponentsInstance.jsonAsyncComponentsCache.forEach((value, key) => { 126 | const chunk = bundle[`${key}.json`] 127 | // eslint-disable-next-line no-sequences 128 | const asyncComponents = Object.entries(value).reduce>((p, [key, value]) => (p[AsyncComponentsInstance.rename(key)] = value.value, p), {}) 129 | 130 | // 命中缓存,说明有需要处理的文件 | 注入`异步组件引用`配置 131 | if (chunk && chunk.type === 'asset' && AsyncComponentsInstance.jsonAsyncComponentsCache.get(key)) { 132 | // 读取 json 文件内容 | 没出错的话一定是 pages-json 133 | const jsonCode = JSON.parse(chunk.source.toString()) 134 | // 缓存原始page-json内容 135 | AsyncComponentsInstance.pageJsonCache.set(key, jsonCode) 136 | 137 | jsonCode.componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key, jsonCode.componentPlaceholder) 138 | 139 | jsonCode.usingComponents = Object.assign(jsonCode.usingComponents || {}, asyncComponents) 140 | chunk.source = JSON.stringify(jsonCode, null, 2) 141 | } 142 | else { 143 | let componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key) 144 | let usingComponents = asyncComponents 145 | const cache = AsyncComponentsInstance.pageJsonCache.get(key) 146 | 147 | if (cache) { 148 | usingComponents = Object.assign(cache.usingComponents || {}, usingComponents) 149 | componentPlaceholder = Object.assign(cache.componentPlaceholder || {}, componentPlaceholder) 150 | } 151 | 152 | bundle[`${key}.json`] = { 153 | type: 'asset', 154 | name: key, 155 | fileName: `${key}.json`, 156 | source: JSON.stringify({ usingComponents, componentPlaceholder }, null, 2), 157 | } as typeof bundle.__proto__ 158 | } 159 | }) 160 | }, 161 | buildStart() { 162 | // 每次新的打包时,清空`异步组件`缓存,主要避免热更新时的缓存问题 163 | AsyncComponentsInstance.jsonAsyncComponentsCache.clear() 164 | AsyncComponentsInstance.scriptDescriptors.clear() 165 | }, 166 | } 167 | } 168 | 169 | /** 170 | * 生成类型定义 171 | */ 172 | function generateModuleDeclaration(parsedResults?: ReturnType, cache?: string[]): string { 173 | let typeDefs = '' 174 | 175 | const prefixList = [ 176 | '/* eslint-disable */', 177 | '/* prettier-ignore */', 178 | '// @ts-nocheck', 179 | '// Generated by @uni-ku/bundle-optimizer', 180 | 'declare module \'*?async\' {', 181 | ' const component: any', 182 | ' export = component', 183 | '}', 184 | ] 185 | prefixList.forEach((prefix) => { 186 | typeDefs += `${prefix}\n` 187 | }) 188 | 189 | // 生成 declare module 语句 190 | function generateDeclareModule(modulePath: string | number, fullPath: string | number) { 191 | typeDefs += `declare module '${fullPath}' {\n` 192 | typeDefs += ` const component: typeof import('${modulePath}')\n` 193 | typeDefs += ` export = component\n` 194 | typeDefs += `}\n` 195 | } 196 | 197 | cache?.forEach((item) => { 198 | const modulePath = item // 模块路径 199 | const fullPath = `${modulePath}?async` 200 | 201 | generateDeclareModule(modulePath, fullPath) 202 | }) 203 | 204 | parsedResults?.filter(item => !cache?.includes(item.modulePath.value.toString())) 205 | .forEach((result) => { 206 | const modulePath = result.modulePath.value // 模块路径 207 | const fullPath = result.fullPath.value 208 | 209 | generateDeclareModule(modulePath, fullPath) 210 | }) 211 | 212 | return typeDefs 213 | } 214 | -------------------------------------------------------------------------------- /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 => 132 | !id.startsWith('\x00')) 133 | temp.forEach((id) => { 134 | acc[moduleIdProcessor(id)] = chunk.fileName 135 | }) 136 | } 137 | } 138 | 139 | return acc 140 | }, {} as Record) 141 | 142 | if (pageComponents.length) { 143 | const chunks = Object.values(bundle) 144 | for (let index = 0; index < chunks.length; index++) { 145 | const chunk = chunks[index] 146 | if (chunk.type === 'chunk') { 147 | const targetKey = Object.keys(hashFileMap).find((key) => { 148 | const value = hashFileMap[key] 149 | return typeof value === 'string' ? chunk.imports.includes(value) : value.some((item: string) => chunk.imports.includes(item)) 150 | }) 151 | if (targetKey) { 152 | const old = typeof hashFileMap[targetKey] === 'string' ? [hashFileMap[targetKey]] : hashFileMap[targetKey] || [] 153 | hashFileMap[targetKey] = [...old, chunk.fileName] 154 | } 155 | } 156 | } 157 | } 158 | 159 | for (const file in bundle) { 160 | const chunk = bundle[file] 161 | if (chunk.type === 'chunk' && chunk.code.includes('AsyncImport')) { 162 | const code = chunk.code 163 | const asyncImports = parseAsyncImports(code) 164 | 165 | if (asyncImports.length > 0) { 166 | const magicString = new MagicString(code) 167 | 168 | asyncImports.forEach(({ full, args }) => { 169 | args.forEach(({ start, end, value }) => { 170 | const url = value.toString() 171 | 172 | // 去除相对路径的前缀,例如`./`、`../`、`../../`等正确的相对路径的写法,`.../`是不正确的 173 | if ( 174 | isMP 175 | ? Object.values(hashFileMap).flat().includes(normalizePath(path.posix.join(path.dirname(chunk.fileName), url))) 176 | : Object.values(hashFileMap).flat().map(resolveAssetsPath).includes(url) 177 | ) { 178 | magicString.overwrite(full.start, full.start + 'AsyncImport'.length, isMP ? 'require.async' : 'import', { contentOnly: true }) 179 | } 180 | }) 181 | }) 182 | // 遍历完毕之后更新chunk的code 183 | chunk.code = magicString.toString() 184 | } 185 | } 186 | } 187 | }, 188 | } 189 | } 190 | 191 | /** 192 | * 生成类型定义 193 | */ 194 | function generateModuleDeclaration(paths?: string[], cache?: string[]): string { 195 | // 将路径组合成 ModuleMap 中的键 196 | const moduleMapEntries = Array.from(new Set([...(cache || []), ...(paths || [])])) 197 | ?.map((p) => { 198 | return ` '${p}': typeof import('${p}')` 199 | }) 200 | .join('\n') 201 | 202 | // 返回类型定义 203 | return `/* eslint-disable */ 204 | /* prettier-ignore */ 205 | // @ts-nocheck 206 | // Generated by @uni-ku/bundle-optimizer 207 | export {} 208 | 209 | interface ModuleMap { 210 | ${moduleMapEntries 211 | ? `${moduleMapEntries} 212 | [path: string]: any` 213 | : ' [path: string]: any' 214 | } 215 | } 216 | 217 | declare global { 218 | function AsyncImport(arg: T): Promise 219 | } 220 | ` 221 | } 222 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils/crypto/readme: -------------------------------------------------------------------------------- 1 | > 本模块主要是对代码字符串hash化的一些工具函数 2 | > 3 | > 部分内容是相关rust库的转写 4 | > 5 | > 暂时没用上 -------------------------------------------------------------------------------- /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/getViteConfigPaths.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 | /** 8 | * 创建一个基于 vite 配置的路径解析函数 9 | * @param config vite 配置 10 | * @returns 路径解析函数 11 | */ 12 | export function createVitePathResolver(config: UserConfig) { 13 | const tempAlias = config.resolve?.alias ?? [] 14 | let alias: Alias[] = [] 15 | if (!Array.isArray(tempAlias)) { 16 | alias = Object.entries(tempAlias as { [find: string]: string }).map(([find, replacement]) => ({ find, replacement })) 17 | } 18 | else { 19 | alias = tempAlias 20 | } 21 | 22 | return (source: string, relative = false) => { 23 | for (let { find, replacement, customResolver: _customResolver } of alias) { 24 | if (!find || !replacement) 25 | continue 26 | 27 | source = normalizePath(source) 28 | if (typeof replacement === 'string' && !isRegExp(replacement) && !replacement.includes('*')) { 29 | replacement = normalizePath(replacement) 30 | } 31 | 32 | if (!isRegExp(replacement) && typeof replacement === 'string' && !replacement.includes('*') 33 | && !isRegExp(find) && typeof find === 'string' && !find.includes('*')) { 34 | if (source === find) { 35 | // 断定为全量匹配 36 | return relative ? replacement : path.resolve(replacement) 37 | } 38 | else if (source.startsWith(find)) { 39 | // 断定为前缀匹配 40 | const realPath = source.replace(find, replacement) 41 | return relative ? realPath : path.resolve(realPath) 42 | } 43 | } 44 | else if (source.match(find) && (isRegExp(find) || !find.includes('*'))) { 45 | const realPath = source.replace(find, replacement) 46 | return relative ? realPath : path.resolve(realPath) 47 | } 48 | } 49 | return source 50 | } 51 | } 52 | 53 | /** vite插件相关的路径解析 | 单例模式 */ 54 | let vitePathResolver: ((source: string, relative?: boolean) => string) | null = null 55 | 56 | export function getVitePathResolver() { 57 | if (!vitePathResolver) { 58 | throw new Error('Vite path resolver has not been initialized. Please call createVitePathResolver first.') 59 | } 60 | return vitePathResolver 61 | } 62 | 63 | export function initializeVitePathResolver(config: UserConfig) { 64 | vitePathResolver = createVitePathResolver(config) 65 | } 66 | -------------------------------------------------------------------------------- /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 | 6 | /** 替换字符串指定位置的字符 */ 7 | export function replaceStringAtPosition(originalStr: string, start: number, end: number, replaceWith: string) { 8 | return originalStr.substring(0, start) + replaceWith + originalStr.substring(end) 9 | } 10 | 11 | /** 转换为斜杠路径 */ 12 | export function slash(p: string): string { 13 | return p.replace(/\\/g, '/') 14 | } 15 | 16 | /** 规范路径 | 处理路径斜杠 */ 17 | export function normalizePath(id: string) { 18 | return process.platform === 'win32' ? slash(id) : id 19 | } 20 | 21 | /** 规范函数语法 */ 22 | export function normalizeFunctionSyntax(funcStr: string, anonymous = false): string { 23 | return funcStr.replace(/^\s*(async\s+)?(function\s+)?([\w$]+)\s*\(/, (match, asyncKeyword, funcKeyword, funcName) => { 24 | return !anonymous && funcName && !['function', 'async'].includes(funcName) 25 | ? `${asyncKeyword || ''}function ${funcName}(` 26 | : `${asyncKeyword || ''}${funcName === 'async' ? padEndStringSpaces(funcName, 1) : 'function'}(` 27 | }) 28 | } 29 | 30 | /** 31 | * 字符串末尾填充空格 32 | * @param str 待处理字符串 33 | * @param count 填充数量 | 默认 0 34 | * @returns 处理后的字符串 | 兜底处理成空字符串 35 | */ 36 | export function padEndStringSpaces(str: string | undefined, count = 0) { 37 | str = str?.toString() 38 | return str?.padEnd(str?.length + count) || '' 39 | } 40 | 41 | /** 检查并创建目录 */ 42 | export function ensureDirectoryExists(filePath: string) { 43 | const dir = path.dirname(filePath) 44 | if (!fs.existsSync(dir)) { 45 | fs.mkdirSync(dir, { recursive: true }) 46 | } 47 | } 48 | 49 | /** 路径处理器 | 去除`rootDir`前缀路径和查询参数 | `rootDir`默认为项目根目录 */ 50 | export function moduleIdProcessor(id: string, rootDir = ROOT_DIR) { 51 | rootDir = normalizePath(rootDir) 52 | // 确保 rootDir 以斜杠结尾 53 | if (!rootDir.endsWith('/')) 54 | rootDir += '/' 55 | 56 | const normalized = normalizePath(id) 57 | const name = normalized.split('?')[0] 58 | // 从name中剔除 rootDir 前缀 59 | const updatedName = name.replace(rootDir, '') 60 | 61 | // 去除来自`node_modules`模块的前缀 62 | if (updatedName.startsWith('\x00')) 63 | return updatedName.slice(1) 64 | 65 | return updatedName 66 | } 67 | 68 | /** 69 | * 计算相对路径的调用层级 70 | * @param importer 引入者文件的路径 71 | * @param imported 被引入文件的路径 72 | * @returns 相对路径前缀 73 | */ 74 | export function calculateRelativePath(importer: string, imported: string): string { 75 | // 获取相对路径 76 | if (imported.match(/^(\.\/|\.\.\/)+/)) { 77 | imported = path.resolve(path.dirname(importer), imported) 78 | } 79 | const relativePath = path.relative(path.dirname(importer), imported) 80 | 81 | // 将路径中的反斜杠替换为正斜杠(适用于 Windows 系统) 82 | return relativePath.replace(/\\/g, '/') 83 | } 84 | 85 | /** 处理 src 前缀的路径 */ 86 | export function resolveSrcPath(id: string) { 87 | return id.replace(SRC_DIR_RE, './') 88 | } 89 | 90 | /** 处理 assets 前缀的路径 */ 91 | export function resolveAssetsPath(id: string) { 92 | return id.replace(ASSETS_DIR_RE, './') 93 | } 94 | 95 | /** 判断是否有后缀 */ 96 | export function hasExtension(id: string) { 97 | return EXT_RE.test(id) 98 | } 99 | 100 | /** 短横线命名法 */ 101 | export function kebabCase(key: string) { 102 | if (!key) 103 | return key 104 | 105 | const result = key.replace(/([A-Z])/g, ' $1').trim() 106 | return result.split(' ').join('-').toLowerCase() 107 | } 108 | 109 | /** 查找第一个不连续的数字 */ 110 | export function findFirstNonConsecutive(arr: number[]): number | null { 111 | if (arr.length < 2) 112 | return null // 如果数组长度小于2,直接返回null 113 | 114 | const result = arr.find((value, index) => index > 0 && value !== arr[index - 1] + 1) 115 | return result !== undefined ? result : null 116 | } 117 | 118 | /** 查找第一个不连续的数字之前的数字 */ 119 | export function findFirstNonConsecutiveBefore(arr: number[]): number | null { 120 | if (arr.length < 2) 121 | return null // 如果数组长度小于2,直接返回null 122 | 123 | const result = arr.find((value, index) => index > 0 && value !== arr[index - 1] + 1) 124 | return (result !== undefined && result !== null) ? arr[arr.indexOf(result) - 1] : null 125 | } 126 | 127 | /** 明确的 bool 型做取反,空值原样返回 */ 128 | export function toggleBoolean(value: boolean | undefined | null) { 129 | return typeof value === 'boolean' ? !value : value 130 | } 131 | 132 | export * from './getViteConfigPaths' 133 | export * from './lex-parse' 134 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/utils/lex-parse/readme: -------------------------------------------------------------------------------- 1 | # lex-parse 2 | 3 | > 实现了一个简单的词法分析器,用于解析代码字符串中的函数调用的场景 4 | > 需要传入代码字符串和需要解析的函数名,返回函数调用的位置信息和参数信息的位置信息 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------