uview-pro 0.6.10 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +14 -4
- package/package.json +1 -1
- package/plugins/index.ts +1 -0
- package/plugins/root/README.md +161 -0
- package/plugins/root/index.ts +123 -0
- package/plugins/root/page.ts +54 -0
- package/plugins/root/root.ts +35 -0
- package/plugins/root/utils.ts +133 -0
- package/readme.md +3 -6
package/changelog.md
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
## 0.6.
|
|
1
|
+
## 0.6.11(2026-07-24)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### ✨ Features | 新功能
|
|
4
4
|
|
|
5
|
-
- **
|
|
6
|
-
- **theme:** 简化初始化主题和多语言相关逻辑 ([ec5d64b](https://github.com/anyup/uView-Pro/commit/ec5d64b0c8e0b494ab21c8c4bf278cf1e2559317))
|
|
5
|
+
- **vite-plugin-uni-root:** 新增内置的 root vite 插件,实现全局根组件自动注入,无需自行引入第三方插件实现 ([f5359a9](https://github.com/anyup/uView-Pro/commit/f5359a92d93150a064f6814a8700cacf272d3556))
|
|
7
6
|
|
|
8
7
|
### 👥 Contributors
|
|
9
8
|
|
|
10
9
|
<a href="https://github.com/anyup"><img src="https://github.com/anyup.png?size=40" width="40" height="40" alt="anyup" title="anyup"/></a>
|
|
11
10
|
|
|
11
|
+
## 0.6.10(2026-07-23)
|
|
12
|
+
|
|
13
|
+
### ♻️ Code Refactoring | 代码重构
|
|
14
|
+
|
|
15
|
+
- **u-config-provider:** 优化暗黑模式和主题初始化逻辑 ([0c010db](https://github.com/anyup/uView-Pro/commit/0c010db5b83bf9514b856cba95a7ba8d9aa56dbf))
|
|
16
|
+
- **theme:** 简化初始化主题和多语言相关逻辑 ([ec5d64b](https://github.com/anyup/uView-Pro/commit/ec5d64b0c8e0b494ab21c8c4bf278cf1e2559317))
|
|
17
|
+
|
|
18
|
+
### 👥 Contributors
|
|
19
|
+
|
|
20
|
+
<a href="https://github.com/anyup"><img src="https://github.com/anyup.png?size=40" width="40" height="40" alt="anyup" title="anyup"/></a>
|
|
21
|
+
|
|
12
22
|
## 0.6.9(2026-07-15)
|
|
13
23
|
|
|
14
24
|
### 🐛 Bug Fixes | Bug 修复
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "uview-pro",
|
|
3
3
|
"name": "uview-pro",
|
|
4
4
|
"displayName": "【支持鸿蒙】uView Pro|基于Vue3+TS的高质量UI组件库,支持多主题、暗黑模式、多语言",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.11",
|
|
6
6
|
"description": "uView Pro是基于Vue3+TS的多平台UI框架,提供80+高质量组件、便捷工具和常用模板,支持多主题、暗黑模式、多语言,支持H5/APP/鸿蒙/小程序多端开发。已在鸿蒙应用商店上架,欢迎体验!",
|
|
7
7
|
"main": "index.ts",
|
|
8
8
|
"module": "index.ts",
|
package/plugins/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './root';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# vite-plugin-uni-root
|
|
2
|
+
|
|
3
|
+
UniApp 虚拟根组件 Vite 插件,零额外依赖,纯字符串操作实现。
|
|
4
|
+
|
|
5
|
+
## 解决的问题
|
|
6
|
+
|
|
7
|
+
UniApp 不支持原生的根组件包裹机制,无法在全局统一注入 `u-config-provider`、`u-toast`、`u-modal` 等需要在每个页面根部存在的组件。传统方案需要在每个页面手动添加,维护成本高。
|
|
8
|
+
|
|
9
|
+
本插件通过 Vite 构建时注入,自动为每个页面包裹 `<global-root-view>` 根组件,无需修改任何页面代码本身。
|
|
10
|
+
|
|
11
|
+
## 技术方案
|
|
12
|
+
|
|
13
|
+
### 架构
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
App.root.vue(源码) → 在 main.ts 中导入并注册为全局组件 global-root-view
|
|
17
|
+
→ 页面模板被包裹 <global-root-view> 页面内容 </global-root-view>
|
|
18
|
+
→ 渲染时页面内容填入 <slot />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 工作流程
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
25
|
+
│ main.ts │ → │ 注入 import │ → │ 全局注册组件 │
|
|
26
|
+
│ │ │ + 注册代码 │ │ global-root-view │
|
|
27
|
+
└─────────────┘ └──────────────┘ └─────────────────┘
|
|
28
|
+
│
|
|
29
|
+
┌─────────────┐ ┌──────────────┐ │
|
|
30
|
+
│ page.vue │ → │ 包裹模板 │ │
|
|
31
|
+
│ │ │ <global-...> │ ←─────────┘
|
|
32
|
+
└─────────────┘ └──────────────┘
|
|
33
|
+
│
|
|
34
|
+
┌─────────────┐ │
|
|
35
|
+
│ App.root.vue│ → <slot /> 接收页面内容
|
|
36
|
+
└─────────────┘
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 三步实现
|
|
40
|
+
|
|
41
|
+
1. **main.ts 注入**(`root.ts -> registerRootApp`)
|
|
42
|
+
- 在 main.ts 中注入 `import GlobalRootView from "./App.root.vue"`
|
|
43
|
+
- 注入 `app.component("global-root-view", GlobalRootView)` 注册为全局组件
|
|
44
|
+
- 注:`GlobalRootView` 仅为局部变量名,实际组件就是 `App.root.vue`
|
|
45
|
+
|
|
46
|
+
2. **页面模板包裹**(`page.ts -> transformPage`)
|
|
47
|
+
- 解析 SFC,定位 `<template>` 标签的起止位置
|
|
48
|
+
- 提取 PageMeta 节点(如有则放到包裹层外部)
|
|
49
|
+
- 将模板内容包裹在 `<global-root-view>...</global-root-view>` 中
|
|
50
|
+
|
|
51
|
+
3. **App.root.vue 加载**(`index.ts -> load`)
|
|
52
|
+
- 拦截 `App.root.vue` 的模块请求,从文件读取并返回
|
|
53
|
+
- 模板中的 `<slot />` 接收并渲染页面内容
|
|
54
|
+
|
|
55
|
+
### 跨端兼容
|
|
56
|
+
|
|
57
|
+
插件是纯 Vite 构建时插件,在 UniApp 编译之前完成源代码转换,不依赖任何平台特定 API,所有 UniApp 支持的平台均可正常工作:
|
|
58
|
+
|
|
59
|
+
| 平台 | 支持 |
|
|
60
|
+
|------|------|
|
|
61
|
+
| H5 | ✅ |
|
|
62
|
+
| 微信小程序 | ✅ |
|
|
63
|
+
| 支付宝小程序 | ✅ |
|
|
64
|
+
| 头条/抖音小程序 | ✅ |
|
|
65
|
+
| Android App | ✅ |
|
|
66
|
+
| iOS App | ✅ |
|
|
67
|
+
| 鸿蒙 App | ✅ |
|
|
68
|
+
|
|
69
|
+
## 使用方式
|
|
70
|
+
|
|
71
|
+
### 1. 创建 App.root.vue
|
|
72
|
+
|
|
73
|
+
在 `src/` 目录下创建 `App.root.vue` 文件:
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<script setup lang="ts">
|
|
77
|
+
import { onLoad } from '@dcloudio/uni-app';
|
|
78
|
+
import { useTheme, useLocale } from 'uview-pro';
|
|
79
|
+
|
|
80
|
+
const { darkMode, themes, currentTheme } = useTheme();
|
|
81
|
+
const { currentLocale } = useLocale();
|
|
82
|
+
|
|
83
|
+
onLoad(() => {
|
|
84
|
+
console.log('darkMode->', darkMode.value);
|
|
85
|
+
console.log('theme->', currentTheme.value?.name);
|
|
86
|
+
console.log('locale->', currentLocale.value?.name);
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<u-config-provider
|
|
92
|
+
:dark-mode="darkMode"
|
|
93
|
+
:themes="themes"
|
|
94
|
+
:current-theme="currentTheme?.name"
|
|
95
|
+
:current-locale="currentLocale?.name"
|
|
96
|
+
>
|
|
97
|
+
<slot />
|
|
98
|
+
<u-toast global></u-toast>
|
|
99
|
+
<u-modal global></u-modal>
|
|
100
|
+
</u-config-provider>
|
|
101
|
+
</template>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
关键点:`<slot />` 是页面内容的渲染位置,必须保留。
|
|
105
|
+
|
|
106
|
+
### 2. 配置 vite.config.ts
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// npm 安装
|
|
110
|
+
import { UniRoot } from 'uview-pro/plugins';
|
|
111
|
+
|
|
112
|
+
// uni_modules 安装
|
|
113
|
+
import { UniRoot } from './src/uni_modules/uview-pro/plugins';
|
|
114
|
+
|
|
115
|
+
export default defineConfig({
|
|
116
|
+
plugins: [
|
|
117
|
+
UniRoot(), // 放在其他插件之前
|
|
118
|
+
// ...其他插件
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. 运行
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm run dev
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
不需要修改任何页面代码,所有页面自动被 `<global-root-view>` 包裹。
|
|
130
|
+
|
|
131
|
+
## 文件结构
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
src/uni_modules/uview-pro/plugins/root/
|
|
135
|
+
├── index.ts # 插件入口,Vite 插件生命周期
|
|
136
|
+
├── root.ts # main.ts 注入 + App.root.vue 处理
|
|
137
|
+
├── page.ts # 页面模板包裹转换
|
|
138
|
+
└── utils.ts # 工具函数(SFC 解析、pages.json 加载、AST 查找等)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 配置项
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
UniRoot({
|
|
145
|
+
rootFileName: 'App.root', // 根组件文件名(不含扩展名),默认 App.root
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## HMR 支持
|
|
150
|
+
|
|
151
|
+
修改 `pages.json` 后自动重载页面列表,无需手动重启开发服务器。
|
|
152
|
+
|
|
153
|
+
## 注意事项
|
|
154
|
+
|
|
155
|
+
- Vue 版本需 >= 3.2.13(使用 `vue/compiler-sfc` 的 `parse` API)
|
|
156
|
+
- 页面模板中如有 `<page-meta>` 组件,会被提取到包裹层外部,确保微信小程序兼容
|
|
157
|
+
- 页面模板中如有嵌套 `<template #slot>` 具名插槽,不影响根模板的正确识别
|
|
158
|
+
|
|
159
|
+
## 致谢
|
|
160
|
+
|
|
161
|
+
本插件的实现参考了 [@uni-ku/root](https://github.com/uni-ku/root)(MIT License,作者 skiyee),核心思路受其启发并进行了重新实现。
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { watch, readFileSync } from 'node:fs';
|
|
5
|
+
import { normalizePath } from 'vite';
|
|
6
|
+
import { registerRootApp, rebuildRootApp } from './root';
|
|
7
|
+
import { transformPage } from './page';
|
|
8
|
+
import { loadPagesJson, normalizePlatformPath, debounce } from './utils';
|
|
9
|
+
|
|
10
|
+
interface UniRootOptions {
|
|
11
|
+
/** 根组件文件名(不含扩展名),默认 App.root */
|
|
12
|
+
rootFileName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 规范化模块 ID:统一路径分隔符,统一 Windows 盘符为小写
|
|
17
|
+
*/
|
|
18
|
+
function normalizeId(id: string) {
|
|
19
|
+
return normalizePath(id).replace(/^[A-Z]:/, match => match.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* UniApp 虚拟根组件 Vite 插件
|
|
24
|
+
*
|
|
25
|
+
* ## 解决的问题
|
|
26
|
+
* UniApp 不支持原生的根组件包裹机制,导致无法在全局统一注入
|
|
27
|
+
* u-config-provider、u-toast、u-modal 等组件。
|
|
28
|
+
*
|
|
29
|
+
* ## 工作原理
|
|
30
|
+
* 1. 在 main.ts 中注入 App.root.vue 的导入,并注册为全局组件 global-root-view
|
|
31
|
+
* 2. 在每个页面的 <template> 中包裹 <global-root-view> 标签
|
|
32
|
+
* 3. App.root.vue 中的 <slot /> 渲染页面内容
|
|
33
|
+
*
|
|
34
|
+
* ## 支持 HMR
|
|
35
|
+
* pages.json 变更时自动重载页面列表,无需手动重启
|
|
36
|
+
*
|
|
37
|
+
* ## 跨端兼容
|
|
38
|
+
* 纯 Vite 构建时插件,在所有 UniApp 支持的平台(H5、微信小程序、支付宝小程序、
|
|
39
|
+
* 头条小程序、Android App、iOS App、鸿蒙 App)均可正常工作
|
|
40
|
+
*/
|
|
41
|
+
export function UniRoot(options?: UniRootOptions): Plugin {
|
|
42
|
+
options = {
|
|
43
|
+
rootFileName: 'App.root',
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const rootPath = process.env.UNI_INPUT_DIR || join(process.cwd(), 'src');
|
|
48
|
+
const appRootPath = resolve(rootPath, `${options.rootFileName}.vue`);
|
|
49
|
+
const pagesPath = resolve(rootPath, 'pages.json');
|
|
50
|
+
|
|
51
|
+
let pagesJson = loadPagesJson(pagesPath, rootPath);
|
|
52
|
+
let watcher: ReturnType<typeof watch> | null = null;
|
|
53
|
+
let hasPlatformPlugin = false;
|
|
54
|
+
|
|
55
|
+
const normalizedAppRoot = normalizeId(appRootPath);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: 'vite-plugin-uni-root',
|
|
59
|
+
enforce: 'pre', // 在其他插件之前执行,确保页面模板先被包裹
|
|
60
|
+
|
|
61
|
+
configResolved({ plugins }) {
|
|
62
|
+
// 检测是否有 uni-platform 插件,用于处理平台特定页面路径(如 .mp-weixin.vue)
|
|
63
|
+
hasPlatformPlugin = plugins.some(v => v.name === 'vite-plugin-uni-platform');
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
configureServer(server) {
|
|
67
|
+
// 监听 pages.json 变更,HMR 热重载页面列表
|
|
68
|
+
const reload = debounce(() => {
|
|
69
|
+
try {
|
|
70
|
+
pagesJson = loadPagesJson(pagesPath, rootPath);
|
|
71
|
+
server.ws.send({ type: 'full-reload' });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('[vite-plugin-uni-root] pages.json reload failed:', e);
|
|
74
|
+
}
|
|
75
|
+
}, 100);
|
|
76
|
+
|
|
77
|
+
watcher = watch(pagesPath, eventType => {
|
|
78
|
+
if (eventType === 'change') reload();
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
load(id) {
|
|
83
|
+
const normalizedId = normalizeId(id);
|
|
84
|
+
// 只拦截根 SFC 请求,不拦截 Vue 子模块请求(如 ?vue&type=script)
|
|
85
|
+
if (normalizedId === normalizedAppRoot) {
|
|
86
|
+
const code = readFileSync(appRootPath, 'utf-8');
|
|
87
|
+
return rebuildRootApp(code).code;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async transform(code, id) {
|
|
92
|
+
const normalizedId = normalizeId(id);
|
|
93
|
+
const normalizedRootPath = normalizeId(rootPath);
|
|
94
|
+
|
|
95
|
+
// 注入 App.root.vue 的导入,并注册为全局组件 global-root-view
|
|
96
|
+
if (normalizedId === `${normalizedRootPath}/main.ts` || normalizedId === `${normalizedRootPath}/main.js`) {
|
|
97
|
+
return registerRootApp(code, options.rootFileName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// App.root.vue 兜底处理(已在 load 中处理,此处只处理根 SFC)
|
|
101
|
+
if (normalizedId === normalizedAppRoot) {
|
|
102
|
+
return rebuildRootApp(code);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 页面模板包裹 <global-root-view>
|
|
106
|
+
const pageId = hasPlatformPlugin ? normalizePlatformPath(normalizedId) : normalizedId;
|
|
107
|
+
const isPage = pagesJson.some(p => pageId === p || pageId.startsWith(`${p}?`));
|
|
108
|
+
if (isPage) {
|
|
109
|
+
return transformPage(code);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
buildEnd() {
|
|
114
|
+
if (watcher) {
|
|
115
|
+
watcher.close();
|
|
116
|
+
watcher = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default UniRoot;
|
|
123
|
+
export type { UniRootOptions };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseSFC, findNode } from './utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 页面模板转换:将页面内容包裹在 <global-root-view> 中
|
|
5
|
+
*
|
|
6
|
+
* <global-root-view> 是 App.root.vue 注册的全局组件,
|
|
7
|
+
* 其模板中包含 <slot />,页面内容通过插槽传入。
|
|
8
|
+
*
|
|
9
|
+
* 处理逻辑:
|
|
10
|
+
* 1. 解析 SFC,定位 <template> 标签的起止位置
|
|
11
|
+
* 2. 提取模板内容,查找 PageMeta 节点(如有则提取到包裹层外部)
|
|
12
|
+
* 3. 将剩余内容包裹在 <global-root-view>...</global-root-view> 中
|
|
13
|
+
*/
|
|
14
|
+
export async function transformPage(code: string) {
|
|
15
|
+
const sfc = await parseSFC(code);
|
|
16
|
+
|
|
17
|
+
if (!sfc.template) return null;
|
|
18
|
+
|
|
19
|
+
// 用正则定位 <template> 开头标签
|
|
20
|
+
const openMatch = code.match(/<template\b[^>]*>/);
|
|
21
|
+
if (!openMatch) return null;
|
|
22
|
+
|
|
23
|
+
// 使用 lastIndexOf 定位最后一个 </template>,即根 <template> 的闭合标签
|
|
24
|
+
// 避免匹配到模板内具名插槽 <template #slot> 的闭合标签
|
|
25
|
+
const closeTagStart = code.lastIndexOf('</template>');
|
|
26
|
+
if (closeTagStart === -1) return null;
|
|
27
|
+
|
|
28
|
+
const openTagEnd = openMatch.index! + openMatch[0].length;
|
|
29
|
+
const content = code.slice(openTagEnd, closeTagStart);
|
|
30
|
+
|
|
31
|
+
// 查找 page-meta 节点(如微信小程序原生的 page-meta 组件)
|
|
32
|
+
const pageMetaNode = findNode(sfc, 'PageMeta');
|
|
33
|
+
let pageMetaSource = '';
|
|
34
|
+
let newContent = content;
|
|
35
|
+
|
|
36
|
+
if (pageMetaNode) {
|
|
37
|
+
pageMetaSource = pageMetaNode.loc.source;
|
|
38
|
+
const metaStart = pageMetaNode.loc.start.offset;
|
|
39
|
+
const metaEnd = pageMetaNode.loc.end.offset;
|
|
40
|
+
|
|
41
|
+
// pageMetaNode.loc 的 offset 是相对于整个文件的,转换为相对于 content 的位置
|
|
42
|
+
const metaStartInContent = metaStart - openTagEnd;
|
|
43
|
+
const metaEndInContent = metaEnd - openTagEnd;
|
|
44
|
+
|
|
45
|
+
newContent = content.slice(0, metaStartInContent) + content.slice(metaEndInContent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// page-meta 放在包裹层外部,内容包裹在 <global-root-view> 中
|
|
49
|
+
const wrappedContent = `\n${pageMetaSource}\n<global-root-view>${newContent}</global-root-view>\n`;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 向 main.ts 注入 App.root.vue 的导入和全局注册
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. 在文件顶部插入 `import GlobalRootView from "./App.root.vue"`
|
|
6
|
+
* 2. 在 createSSRApp 之后插入 `app.component("global-root-view", GlobalRootView)`
|
|
7
|
+
*
|
|
8
|
+
* 注:GlobalRootView 仅为 main.ts 中的局部变量名,
|
|
9
|
+
* 实际组件就是 App.root.vue,页面通过 <global-root-view> 标签使用。
|
|
10
|
+
*/
|
|
11
|
+
export function registerRootApp(code: string, fileName: string = 'App.root') {
|
|
12
|
+
const importCode = `import GlobalRootView from "./${fileName}.vue";`;
|
|
13
|
+
const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
|
|
14
|
+
|
|
15
|
+
let newCode = importCode + '\n' + code;
|
|
16
|
+
|
|
17
|
+
// 在 createSSRApp 赋值语句之后插入组件注册代码
|
|
18
|
+
const ssrAppMatch = newCode.match(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/);
|
|
19
|
+
if (ssrAppMatch) {
|
|
20
|
+
newCode = newCode.replace(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/, `$1\n ${vueUseComponentCode}`);
|
|
21
|
+
} else {
|
|
22
|
+
// 兜底:在 return { 之前插入
|
|
23
|
+
newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}\n $2`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { code: newCode };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* App.root.vue 模板中已使用 <slot /> 作为子内容插槽,
|
|
31
|
+
* 无需替换,直接透传。
|
|
32
|
+
*/
|
|
33
|
+
export function rebuildRootApp(code: string) {
|
|
34
|
+
return { code };
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { normalizePath } from 'vite';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse Vue SFC file, return descriptor
|
|
7
|
+
*/
|
|
8
|
+
export async function parseSFC(code: string) {
|
|
9
|
+
try {
|
|
10
|
+
const { parse } = await import('vue/compiler-sfc');
|
|
11
|
+
return parse(code, { pad: 'space' }).descriptor;
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error('[vite-plugin-uni-root] Vue version must be 3.2.13 or higher.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Strip JSON comments (single-line and multi-line)
|
|
19
|
+
*/
|
|
20
|
+
export function stripJsonComments(str: string): string {
|
|
21
|
+
return str.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert pages.json path to absolute file path
|
|
26
|
+
*/
|
|
27
|
+
export function formatPagePath(root: string, path: string) {
|
|
28
|
+
return normalizePath(`${join(root, path)}.vue`).replace(/^[A-Z]:/, match => match.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load pages.json and resolve all page paths (main + subPackages)
|
|
33
|
+
*/
|
|
34
|
+
export function loadPagesJson(path: string, rootPath: string): string[] {
|
|
35
|
+
const raw = readFileSync(path, 'utf-8');
|
|
36
|
+
const { pages = [], subPackages = [] } = JSON.parse(stripJsonComments(raw));
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
...pages.map((page: any) => formatPagePath(rootPath, page.path)),
|
|
40
|
+
...subPackages
|
|
41
|
+
.map(({ pages: subPages = [], root = '' }: any) => {
|
|
42
|
+
return subPages.map((page: any) => formatPagePath(join(rootPath, root), page.path));
|
|
43
|
+
})
|
|
44
|
+
.flat()
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* camelCase to kebab-case: PageMeta -> page-meta
|
|
50
|
+
*/
|
|
51
|
+
export function toKebabCase(str: string) {
|
|
52
|
+
return str
|
|
53
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
54
|
+
.replace(/[_\s]+/g, '-')
|
|
55
|
+
.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* kebab-case to PascalCase: page-meta -> PageMeta
|
|
60
|
+
*/
|
|
61
|
+
export function toPascalCase(str: string) {
|
|
62
|
+
return str.replace(/(^\w|-+\w)/g, match => match.toUpperCase().replace(/-/g, ''));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface TagNode {
|
|
66
|
+
loc: {
|
|
67
|
+
source: string;
|
|
68
|
+
start: { offset: number };
|
|
69
|
+
end: { offset: number };
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find a tag node in SFC template AST by tag name
|
|
75
|
+
* Supports both kebab-case and PascalCase
|
|
76
|
+
*/
|
|
77
|
+
export function findNode(sfc: any, rawTagName: string): TagNode | undefined {
|
|
78
|
+
const templateSource = sfc.template?.content;
|
|
79
|
+
if (!templateSource) return;
|
|
80
|
+
|
|
81
|
+
let tagName = '';
|
|
82
|
+
if (templateSource.includes(`<${toKebabCase(rawTagName)}`)) {
|
|
83
|
+
tagName = toKebabCase(rawTagName);
|
|
84
|
+
} else if (templateSource.includes(`<${toPascalCase(rawTagName)}`)) {
|
|
85
|
+
tagName = toPascalCase(rawTagName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!tagName) return;
|
|
89
|
+
|
|
90
|
+
const nodeAst = sfc.template?.ast;
|
|
91
|
+
if (!nodeAst) return;
|
|
92
|
+
|
|
93
|
+
// Recursively traverse AST to find the target node
|
|
94
|
+
const traverse = (nodes: any): TagNode | undefined => {
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
if (node.type === 1) {
|
|
97
|
+
if (node.tag === tagName) return node;
|
|
98
|
+
if (node.children?.length) {
|
|
99
|
+
const found = traverse(node.children);
|
|
100
|
+
if (found) return found;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return traverse(nodeAst.children);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Normalize platform-specific page paths (.mp-weixin.vue -> .vue)
|
|
112
|
+
*/
|
|
113
|
+
export function normalizePlatformPath(id: string) {
|
|
114
|
+
const platform = process.env.UNI_PLATFORM;
|
|
115
|
+
if (!platform) return id;
|
|
116
|
+
|
|
117
|
+
const regex = new RegExp(`\\.${platform}\\.vue$`);
|
|
118
|
+
if (regex.test(id)) {
|
|
119
|
+
return id.replace(`.${platform}.`, '.');
|
|
120
|
+
}
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Debounce utility
|
|
126
|
+
*/
|
|
127
|
+
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number) {
|
|
128
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
129
|
+
return (...args: Parameters<T>) => {
|
|
130
|
+
if (timer) clearTimeout(timer);
|
|
131
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
132
|
+
};
|
|
133
|
+
}
|
package/readme.md
CHANGED
|
@@ -229,11 +229,8 @@ export function createApp() {
|
|
|
229
229
|
|
|
230
230
|
`uView Pro` 遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将 `uView Pro` 应用到您的产品中。
|
|
231
231
|
|
|
232
|
-
##
|
|
232
|
+
## 致谢
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
uView Pro 基于 [uView UI 1.8.8](https://github.com/umicro/uView) 进行 TypeScript 完全重构,在此向 uView UI 原作者及所有贡献者致以诚挚感谢。
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
- [Gitee](https://gitee.com/anyup/uview-pro)
|
|
238
|
-
- [uView UI 1.0](https://github.com/umicro/uView)
|
|
239
|
-
- [uView UI 2.0](https://github.com/umicro/uView2.0)
|
|
236
|
+
同时感谢每一位为 uView Pro 提交 Issue、PR 和改进建议的社区开发者。
|