uview-pro 0.6.10 → 0.6.12

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 CHANGED
@@ -1,14 +1,34 @@
1
- ## 0.6.10(2026-07-23
1
+ ## 0.6.12(2026-07-24
2
2
 
3
- ### ♻️ Code Refactoring | 代码重构
3
+ ### 🐛 Bug Fixes | Bug 修复
4
4
 
5
- - **u-config-provider:** 优化暗黑模式和主题初始化逻辑 ([0c010db](https://github.com/anyup/uView-Pro/commit/0c010db5b83bf9514b856cba95a7ba8d9aa56dbf))
6
- - **theme:** 简化初始化主题和多语言相关逻辑 ([ec5d64b](https://github.com/anyup/uView-Pro/commit/ec5d64b0c8e0b494ab21c8c4bf278cf1e2559317))
5
+ - **vite:** 修复vite插件root根组件在使用npm方式引入时导致无法被正确加载的问题 ([90561fa](https://github.com/anyup/uView-Pro/commit/90561fa406a8c9d8d49f8223cdda333cc662a42a))
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.11(2026-07-24)
12
+
13
+ ### ✨ Features | 新功能
14
+
15
+ - **vite-plugin-uni-root:** 新增内置的 root vite 插件,实现全局根组件自动注入,无需自行引入第三方插件实现 ([f5359a9](https://github.com/anyup/uView-Pro/commit/f5359a92d93150a064f6814a8700cacf272d3556))
16
+
17
+ ### 👥 Contributors
18
+
19
+ <a href="https://github.com/anyup"><img src="https://github.com/anyup.png?size=40" width="40" height="40" alt="anyup" title="anyup"/></a>
20
+
21
+ ## 0.6.10(2026-07-23)
22
+
23
+ ### ♻️ Code Refactoring | 代码重构
24
+
25
+ - **u-config-provider:** 优化暗黑模式和主题初始化逻辑 ([0c010db](https://github.com/anyup/uView-Pro/commit/0c010db5b83bf9514b856cba95a7ba8d9aa56dbf))
26
+ - **theme:** 简化初始化主题和多语言相关逻辑 ([ec5d64b](https://github.com/anyup/uView-Pro/commit/ec5d64b0c8e0b494ab21c8c4bf278cf1e2559317))
27
+
28
+ ### 👥 Contributors
29
+
30
+ <a href="https://github.com/anyup"><img src="https://github.com/anyup.png?size=40" width="40" height="40" alt="anyup" title="anyup"/></a>
31
+
12
32
  ## 0.6.9(2026-07-15)
13
33
 
14
34
  ### 🐛 Bug Fixes | Bug 修复
package/package.json CHANGED
@@ -2,11 +2,21 @@
2
2
  "id": "uview-pro",
3
3
  "name": "uview-pro",
4
4
  "displayName": "【支持鸿蒙】uView Pro|基于Vue3+TS的高质量UI组件库,支持多主题、暗黑模式、多语言",
5
- "version": "0.6.10",
5
+ "version": "0.6.12",
6
6
  "description": "uView Pro是基于Vue3+TS的多平台UI框架,提供80+高质量组件、便捷工具和常用模板,支持多主题、暗黑模式、多语言,支持H5/APP/鸿蒙/小程序多端开发。已在鸿蒙应用商店上架,欢迎体验!",
7
7
  "main": "index.ts",
8
8
  "module": "index.ts",
9
9
  "browser": "index.ts",
10
+ "exports": {
11
+ ".": "./index.ts",
12
+ "./plugins": {
13
+ "types": "./plugins/index.ts",
14
+ "import": "./plugins/index.mjs",
15
+ "require": "./plugins/index.cjs",
16
+ "default": "./plugins/index.mjs"
17
+ },
18
+ "./*": "./*"
19
+ },
10
20
  "keywords": [
11
21
  "uview-pro",
12
22
  "vue3",
@@ -0,0 +1 @@
1
+ module.exports = require("./root/dist/index.cjs");
@@ -0,0 +1 @@
1
+ export * from "./root/dist/index.mjs";
@@ -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,111 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var root_exports = {};
29
+ __export(root_exports, {
30
+ UniRoot: () => UniRoot,
31
+ default: () => root_default
32
+ });
33
+ module.exports = __toCommonJS(root_exports);
34
+ var import_node_path = require("node:path");
35
+ var import_node_process = __toESM(require("node:process"));
36
+ var import_node_fs = require("node:fs");
37
+ var import_vite = require("vite");
38
+ var import_root = require("./root.cjs");
39
+ var import_page = require("./page.cjs");
40
+ var import_utils = require("./utils.cjs");
41
+ function normalizeId(id) {
42
+ return (0, import_vite.normalizePath)(id).replace(/^[A-Z]:/, (match) => match.toLowerCase());
43
+ }
44
+ function UniRoot(options) {
45
+ options = {
46
+ rootFileName: "App.root",
47
+ ...options
48
+ };
49
+ const rootPath = import_node_process.default.env.UNI_INPUT_DIR || (0, import_node_path.join)(import_node_process.default.cwd(), "src");
50
+ const appRootPath = (0, import_node_path.resolve)(rootPath, `${options.rootFileName}.vue`);
51
+ const pagesPath = (0, import_node_path.resolve)(rootPath, "pages.json");
52
+ let pagesJson = (0, import_utils.loadPagesJson)(pagesPath, rootPath);
53
+ let watcher = null;
54
+ let hasPlatformPlugin = false;
55
+ const normalizedAppRoot = normalizeId(appRootPath);
56
+ return {
57
+ name: "vite-plugin-uni-root",
58
+ enforce: "pre",
59
+ // 在其他插件之前执行,确保页面模板先被包裹
60
+ configResolved({ plugins }) {
61
+ hasPlatformPlugin = plugins.some((v) => v.name === "vite-plugin-uni-platform");
62
+ },
63
+ configureServer(server) {
64
+ const reload = (0, import_utils.debounce)(() => {
65
+ try {
66
+ pagesJson = (0, import_utils.loadPagesJson)(pagesPath, rootPath);
67
+ server.ws.send({ type: "full-reload" });
68
+ } catch (e) {
69
+ console.error("[vite-plugin-uni-root] pages.json reload failed:", e);
70
+ }
71
+ }, 100);
72
+ watcher = (0, import_node_fs.watch)(pagesPath, (eventType) => {
73
+ if (eventType === "change")
74
+ reload();
75
+ });
76
+ },
77
+ load(id) {
78
+ const normalizedId = normalizeId(id);
79
+ if (normalizedId === normalizedAppRoot) {
80
+ const code = (0, import_node_fs.readFileSync)(appRootPath, "utf-8");
81
+ return (0, import_root.rebuildRootApp)(code).code;
82
+ }
83
+ },
84
+ async transform(code, id) {
85
+ const normalizedId = normalizeId(id);
86
+ const normalizedRootPath = normalizeId(rootPath);
87
+ if (normalizedId === `${normalizedRootPath}/main.ts` || normalizedId === `${normalizedRootPath}/main.js`) {
88
+ return (0, import_root.registerRootApp)(code, options.rootFileName);
89
+ }
90
+ if (normalizedId === normalizedAppRoot) {
91
+ return (0, import_root.rebuildRootApp)(code);
92
+ }
93
+ const pageId = hasPlatformPlugin ? (0, import_utils.normalizePlatformPath)(normalizedId) : normalizedId;
94
+ const isPage = pagesJson.some((p) => pageId === p || pageId.startsWith(`${p}?`));
95
+ if (isPage) {
96
+ return (0, import_page.transformPage)(code);
97
+ }
98
+ },
99
+ buildEnd() {
100
+ if (watcher) {
101
+ watcher.close();
102
+ watcher = null;
103
+ }
104
+ }
105
+ };
106
+ }
107
+ var root_default = UniRoot;
108
+ // Annotate the CommonJS export names for ESM import in node:
109
+ 0 && (module.exports = {
110
+ UniRoot
111
+ });
@@ -0,0 +1,78 @@
1
+ import { resolve, join } from "node:path";
2
+ import process from "node:process";
3
+ import { watch, readFileSync } from "node:fs";
4
+ import { normalizePath } from "vite";
5
+ import { registerRootApp, rebuildRootApp } from "./root.mjs";
6
+ import { transformPage } from "./page.mjs";
7
+ import { loadPagesJson, normalizePlatformPath, debounce } from "./utils.mjs";
8
+ function normalizeId(id) {
9
+ return normalizePath(id).replace(/^[A-Z]:/, (match) => match.toLowerCase());
10
+ }
11
+ function UniRoot(options) {
12
+ options = {
13
+ rootFileName: "App.root",
14
+ ...options
15
+ };
16
+ const rootPath = process.env.UNI_INPUT_DIR || join(process.cwd(), "src");
17
+ const appRootPath = resolve(rootPath, `${options.rootFileName}.vue`);
18
+ const pagesPath = resolve(rootPath, "pages.json");
19
+ let pagesJson = loadPagesJson(pagesPath, rootPath);
20
+ let watcher = null;
21
+ let hasPlatformPlugin = false;
22
+ const normalizedAppRoot = normalizeId(appRootPath);
23
+ return {
24
+ name: "vite-plugin-uni-root",
25
+ enforce: "pre",
26
+ // 在其他插件之前执行,确保页面模板先被包裹
27
+ configResolved({ plugins }) {
28
+ hasPlatformPlugin = plugins.some((v) => v.name === "vite-plugin-uni-platform");
29
+ },
30
+ configureServer(server) {
31
+ const reload = debounce(() => {
32
+ try {
33
+ pagesJson = loadPagesJson(pagesPath, rootPath);
34
+ server.ws.send({ type: "full-reload" });
35
+ } catch (e) {
36
+ console.error("[vite-plugin-uni-root] pages.json reload failed:", e);
37
+ }
38
+ }, 100);
39
+ watcher = watch(pagesPath, (eventType) => {
40
+ if (eventType === "change")
41
+ reload();
42
+ });
43
+ },
44
+ load(id) {
45
+ const normalizedId = normalizeId(id);
46
+ if (normalizedId === normalizedAppRoot) {
47
+ const code = readFileSync(appRootPath, "utf-8");
48
+ return rebuildRootApp(code).code;
49
+ }
50
+ },
51
+ async transform(code, id) {
52
+ const normalizedId = normalizeId(id);
53
+ const normalizedRootPath = normalizeId(rootPath);
54
+ if (normalizedId === `${normalizedRootPath}/main.ts` || normalizedId === `${normalizedRootPath}/main.js`) {
55
+ return registerRootApp(code, options.rootFileName);
56
+ }
57
+ if (normalizedId === normalizedAppRoot) {
58
+ return rebuildRootApp(code);
59
+ }
60
+ const pageId = hasPlatformPlugin ? normalizePlatformPath(normalizedId) : normalizedId;
61
+ const isPage = pagesJson.some((p) => pageId === p || pageId.startsWith(`${p}?`));
62
+ if (isPage) {
63
+ return transformPage(code);
64
+ }
65
+ },
66
+ buildEnd() {
67
+ if (watcher) {
68
+ watcher.close();
69
+ watcher = null;
70
+ }
71
+ }
72
+ };
73
+ }
74
+ var root_default = UniRoot;
75
+ export {
76
+ UniRoot,
77
+ root_default as default
78
+ };
@@ -0,0 +1,58 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var page_exports = {};
19
+ __export(page_exports, {
20
+ transformPage: () => transformPage
21
+ });
22
+ module.exports = __toCommonJS(page_exports);
23
+ var import_utils = require("./utils.cjs");
24
+ async function transformPage(code) {
25
+ const sfc = await (0, import_utils.parseSFC)(code);
26
+ if (!sfc.template)
27
+ return null;
28
+ const openMatch = code.match(/<template\b[^>]*>/);
29
+ if (!openMatch)
30
+ return null;
31
+ const closeTagStart = code.lastIndexOf("</template>");
32
+ if (closeTagStart === -1)
33
+ return null;
34
+ const openTagEnd = openMatch.index + openMatch[0].length;
35
+ const content = code.slice(openTagEnd, closeTagStart);
36
+ const pageMetaNode = (0, import_utils.findNode)(sfc, "PageMeta");
37
+ let pageMetaSource = "";
38
+ let newContent = content;
39
+ if (pageMetaNode) {
40
+ pageMetaSource = pageMetaNode.loc.source;
41
+ const metaStart = pageMetaNode.loc.start.offset;
42
+ const metaEnd = pageMetaNode.loc.end.offset;
43
+ const metaStartInContent = metaStart - openTagEnd;
44
+ const metaEndInContent = metaEnd - openTagEnd;
45
+ newContent = content.slice(0, metaStartInContent) + content.slice(metaEndInContent);
46
+ }
47
+ const wrappedContent = `
48
+ ${pageMetaSource}
49
+ <global-root-view>${newContent}</global-root-view>
50
+ `;
51
+ return {
52
+ code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart)
53
+ };
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ transformPage
58
+ });
@@ -0,0 +1,35 @@
1
+ import { parseSFC, findNode } from "./utils.mjs";
2
+ async function transformPage(code) {
3
+ const sfc = await parseSFC(code);
4
+ if (!sfc.template)
5
+ return null;
6
+ const openMatch = code.match(/<template\b[^>]*>/);
7
+ if (!openMatch)
8
+ return null;
9
+ const closeTagStart = code.lastIndexOf("</template>");
10
+ if (closeTagStart === -1)
11
+ return null;
12
+ const openTagEnd = openMatch.index + openMatch[0].length;
13
+ const content = code.slice(openTagEnd, closeTagStart);
14
+ const pageMetaNode = findNode(sfc, "PageMeta");
15
+ let pageMetaSource = "";
16
+ let newContent = content;
17
+ if (pageMetaNode) {
18
+ pageMetaSource = pageMetaNode.loc.source;
19
+ const metaStart = pageMetaNode.loc.start.offset;
20
+ const metaEnd = pageMetaNode.loc.end.offset;
21
+ const metaStartInContent = metaStart - openTagEnd;
22
+ const metaEndInContent = metaEnd - openTagEnd;
23
+ newContent = content.slice(0, metaStartInContent) + content.slice(metaEndInContent);
24
+ }
25
+ const wrappedContent = `
26
+ ${pageMetaSource}
27
+ <global-root-view>${newContent}</global-root-view>
28
+ `;
29
+ return {
30
+ code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart)
31
+ };
32
+ }
33
+ export {
34
+ transformPage
35
+ };
@@ -0,0 +1,45 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var root_exports = {};
19
+ __export(root_exports, {
20
+ rebuildRootApp: () => rebuildRootApp,
21
+ registerRootApp: () => registerRootApp
22
+ });
23
+ module.exports = __toCommonJS(root_exports);
24
+ function registerRootApp(code, fileName = "App.root") {
25
+ const importCode = `import GlobalRootView from "./${fileName}.vue";`;
26
+ const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
27
+ let newCode = importCode + "\n" + code;
28
+ const ssrAppMatch = newCode.match(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/);
29
+ if (ssrAppMatch) {
30
+ newCode = newCode.replace(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/, `$1
31
+ ${vueUseComponentCode}`);
32
+ } else {
33
+ newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}
34
+ $2`);
35
+ }
36
+ return { code: newCode };
37
+ }
38
+ function rebuildRootApp(code) {
39
+ return { code };
40
+ }
41
+ // Annotate the CommonJS export names for ESM import in node:
42
+ 0 && (module.exports = {
43
+ rebuildRootApp,
44
+ registerRootApp
45
+ });
@@ -0,0 +1,21 @@
1
+ function registerRootApp(code, fileName = "App.root") {
2
+ const importCode = `import GlobalRootView from "./${fileName}.vue";`;
3
+ const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
4
+ let newCode = importCode + "\n" + code;
5
+ const ssrAppMatch = newCode.match(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/);
6
+ if (ssrAppMatch) {
7
+ newCode = newCode.replace(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/, `$1
8
+ ${vueUseComponentCode}`);
9
+ } else {
10
+ newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}
11
+ $2`);
12
+ }
13
+ return { code: newCode };
14
+ }
15
+ function rebuildRootApp(code) {
16
+ return { code };
17
+ }
18
+ export {
19
+ rebuildRootApp,
20
+ registerRootApp
21
+ };
@@ -0,0 +1,136 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var utils_exports = {};
29
+ __export(utils_exports, {
30
+ debounce: () => debounce,
31
+ findNode: () => findNode,
32
+ formatPagePath: () => formatPagePath,
33
+ loadPagesJson: () => loadPagesJson,
34
+ normalizePlatformPath: () => normalizePlatformPath,
35
+ parseSFC: () => parseSFC,
36
+ stripJsonComments: () => stripJsonComments,
37
+ toKebabCase: () => toKebabCase,
38
+ toPascalCase: () => toPascalCase
39
+ });
40
+ module.exports = __toCommonJS(utils_exports);
41
+ var import_node_fs = require("node:fs");
42
+ var import_node_path = require("node:path");
43
+ var import_vite = require("vite");
44
+ async function parseSFC(code) {
45
+ try {
46
+ const { parse } = await import("vue/compiler-sfc");
47
+ return parse(code, { pad: "space" }).descriptor;
48
+ } catch {
49
+ throw new Error("[vite-plugin-uni-root] Vue version must be 3.2.13 or higher.");
50
+ }
51
+ }
52
+ function stripJsonComments(str) {
53
+ return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
54
+ }
55
+ function formatPagePath(root, path) {
56
+ return (0, import_vite.normalizePath)(`${(0, import_node_path.join)(root, path)}.vue`).replace(/^[A-Z]:/, (match) => match.toLowerCase());
57
+ }
58
+ function loadPagesJson(path, rootPath) {
59
+ const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
60
+ const { pages = [], subPackages = [] } = JSON.parse(stripJsonComments(raw));
61
+ return [
62
+ ...pages.map((page) => formatPagePath(rootPath, page.path)),
63
+ ...subPackages.map(({ pages: subPages = [], root = "" }) => {
64
+ return subPages.map((page) => formatPagePath((0, import_node_path.join)(rootPath, root), page.path));
65
+ }).flat()
66
+ ];
67
+ }
68
+ function toKebabCase(str) {
69
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
70
+ }
71
+ function toPascalCase(str) {
72
+ return str.replace(/(^\w|-+\w)/g, (match) => match.toUpperCase().replace(/-/g, ""));
73
+ }
74
+ function findNode(sfc, rawTagName) {
75
+ var _a, _b;
76
+ const templateSource = (_a = sfc.template) == null ? void 0 : _a.content;
77
+ if (!templateSource)
78
+ return;
79
+ let tagName = "";
80
+ if (templateSource.includes(`<${toKebabCase(rawTagName)}`)) {
81
+ tagName = toKebabCase(rawTagName);
82
+ } else if (templateSource.includes(`<${toPascalCase(rawTagName)}`)) {
83
+ tagName = toPascalCase(rawTagName);
84
+ }
85
+ if (!tagName)
86
+ return;
87
+ const nodeAst = (_b = sfc.template) == null ? void 0 : _b.ast;
88
+ if (!nodeAst)
89
+ return;
90
+ const traverse = (nodes) => {
91
+ var _a2;
92
+ for (const node of nodes) {
93
+ if (node.type === 1) {
94
+ if (node.tag === tagName)
95
+ return node;
96
+ if ((_a2 = node.children) == null ? void 0 : _a2.length) {
97
+ const found = traverse(node.children);
98
+ if (found)
99
+ return found;
100
+ }
101
+ }
102
+ }
103
+ return void 0;
104
+ };
105
+ return traverse(nodeAst.children);
106
+ }
107
+ function normalizePlatformPath(id) {
108
+ const platform = process.env.UNI_PLATFORM;
109
+ if (!platform)
110
+ return id;
111
+ const regex = new RegExp(`\\.${platform}\\.vue$`);
112
+ if (regex.test(id)) {
113
+ return id.replace(`.${platform}.`, ".");
114
+ }
115
+ return id;
116
+ }
117
+ function debounce(fn, delay) {
118
+ let timer = null;
119
+ return (...args) => {
120
+ if (timer)
121
+ clearTimeout(timer);
122
+ timer = setTimeout(() => fn(...args), delay);
123
+ };
124
+ }
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ debounce,
128
+ findNode,
129
+ formatPagePath,
130
+ loadPagesJson,
131
+ normalizePlatformPath,
132
+ parseSFC,
133
+ stripJsonComments,
134
+ toKebabCase,
135
+ toPascalCase
136
+ });
@@ -0,0 +1,95 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { normalizePath } from "vite";
4
+ async function parseSFC(code) {
5
+ try {
6
+ const { parse } = await import("vue/compiler-sfc");
7
+ return parse(code, { pad: "space" }).descriptor;
8
+ } catch {
9
+ throw new Error("[vite-plugin-uni-root] Vue version must be 3.2.13 or higher.");
10
+ }
11
+ }
12
+ function stripJsonComments(str) {
13
+ return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
14
+ }
15
+ function formatPagePath(root, path) {
16
+ return normalizePath(`${join(root, path)}.vue`).replace(/^[A-Z]:/, (match) => match.toLowerCase());
17
+ }
18
+ function loadPagesJson(path, rootPath) {
19
+ const raw = readFileSync(path, "utf-8");
20
+ const { pages = [], subPackages = [] } = JSON.parse(stripJsonComments(raw));
21
+ return [
22
+ ...pages.map((page) => formatPagePath(rootPath, page.path)),
23
+ ...subPackages.map(({ pages: subPages = [], root = "" }) => {
24
+ return subPages.map((page) => formatPagePath(join(rootPath, root), page.path));
25
+ }).flat()
26
+ ];
27
+ }
28
+ function toKebabCase(str) {
29
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
30
+ }
31
+ function toPascalCase(str) {
32
+ return str.replace(/(^\w|-+\w)/g, (match) => match.toUpperCase().replace(/-/g, ""));
33
+ }
34
+ function findNode(sfc, rawTagName) {
35
+ var _a, _b;
36
+ const templateSource = (_a = sfc.template) == null ? void 0 : _a.content;
37
+ if (!templateSource)
38
+ return;
39
+ let tagName = "";
40
+ if (templateSource.includes(`<${toKebabCase(rawTagName)}`)) {
41
+ tagName = toKebabCase(rawTagName);
42
+ } else if (templateSource.includes(`<${toPascalCase(rawTagName)}`)) {
43
+ tagName = toPascalCase(rawTagName);
44
+ }
45
+ if (!tagName)
46
+ return;
47
+ const nodeAst = (_b = sfc.template) == null ? void 0 : _b.ast;
48
+ if (!nodeAst)
49
+ return;
50
+ const traverse = (nodes) => {
51
+ var _a2;
52
+ for (const node of nodes) {
53
+ if (node.type === 1) {
54
+ if (node.tag === tagName)
55
+ return node;
56
+ if ((_a2 = node.children) == null ? void 0 : _a2.length) {
57
+ const found = traverse(node.children);
58
+ if (found)
59
+ return found;
60
+ }
61
+ }
62
+ }
63
+ return void 0;
64
+ };
65
+ return traverse(nodeAst.children);
66
+ }
67
+ function normalizePlatformPath(id) {
68
+ const platform = process.env.UNI_PLATFORM;
69
+ if (!platform)
70
+ return id;
71
+ const regex = new RegExp(`\\.${platform}\\.vue$`);
72
+ if (regex.test(id)) {
73
+ return id.replace(`.${platform}.`, ".");
74
+ }
75
+ return id;
76
+ }
77
+ function debounce(fn, delay) {
78
+ let timer = null;
79
+ return (...args) => {
80
+ if (timer)
81
+ clearTimeout(timer);
82
+ timer = setTimeout(() => fn(...args), delay);
83
+ };
84
+ }
85
+ export {
86
+ debounce,
87
+ findNode,
88
+ formatPagePath,
89
+ loadPagesJson,
90
+ normalizePlatformPath,
91
+ parseSFC,
92
+ stripJsonComments,
93
+ toKebabCase,
94
+ toPascalCase
95
+ };
@@ -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
- 再次感谢 `uView UI` 开发团队,以及所有为 `uView UI` 的贡献者,以及所有为 `uView Pro` 的贡献者。
234
+ uView Pro 基于 [uView UI 1.8.8](https://github.com/umicro/uView) 进行 TypeScript 完全重构,在此向 uView UI 原作者及所有贡献者致以诚挚感谢。
235
235
 
236
- - [Github](https://github.com/anyup/uview-pro)
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 和改进建议的社区开发者。