uview-pro 0.6.13 → 0.6.14
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 +22 -3
- package/index.ts +100 -100
- package/package.json +2 -11
- package/plugins/root/README.md +161 -161
- package/plugins/root/dist/page.cjs +29 -11
- package/plugins/root/dist/page.mjs +29 -11
- package/plugins/root/dist/root.cjs +10 -2
- package/plugins/root/dist/root.mjs +10 -2
- package/plugins/root/dist/utils.cjs +1 -1
- package/plugins/root/dist/utils.mjs +1 -1
- package/plugins/root/index.ts +123 -123
- package/plugins/root/page.ts +77 -54
- package/plugins/root/root.ts +53 -35
- package/plugins/root/utils.ts +137 -133
- package/readme.md +2 -2
- package/types/index.d.ts +14 -19
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { parseSFC, findNode } from "./utils.mjs";
|
|
2
2
|
async function transformPage(code) {
|
|
3
3
|
const sfc = await parseSFC(code);
|
|
4
|
-
if (!sfc.template)
|
|
5
|
-
return null;
|
|
6
4
|
const openMatch = code.match(/<template\b[^>]*>/);
|
|
7
5
|
if (!openMatch)
|
|
8
6
|
return null;
|
|
@@ -11,23 +9,43 @@ async function transformPage(code) {
|
|
|
11
9
|
return null;
|
|
12
10
|
const openTagEnd = openMatch.index + openMatch[0].length;
|
|
13
11
|
const content = code.slice(openTagEnd, closeTagStart);
|
|
14
|
-
const pageMetaNode = findNode(sfc, "PageMeta");
|
|
15
12
|
let pageMetaSource = "";
|
|
16
13
|
let newContent = content;
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
if (sfc == null ? void 0 : sfc.template) {
|
|
15
|
+
const pageMetaNode = findNode(sfc, "PageMeta");
|
|
16
|
+
if (pageMetaNode) {
|
|
17
|
+
pageMetaSource = pageMetaNode.loc.source;
|
|
18
|
+
const metaStart = pageMetaNode.loc.start.offset;
|
|
19
|
+
const metaEnd = pageMetaNode.loc.end.offset;
|
|
20
|
+
const metaStartInContent = metaStart - openTagEnd;
|
|
21
|
+
const metaEndInContent = metaEnd - openTagEnd;
|
|
22
|
+
newContent = content.slice(0, metaStartInContent) + content.slice(metaEndInContent);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
const pageMetaMatch = content.match(/<page-meta\b[^>]*>[\s\S]*?<\/page-meta>/);
|
|
26
|
+
if (pageMetaMatch) {
|
|
27
|
+
pageMetaSource = pageMetaMatch[0];
|
|
28
|
+
newContent = content.replace(pageMetaMatch[0], "");
|
|
29
|
+
} else {
|
|
30
|
+
const selfClosingMatch = content.match(/<page-meta\b[^>]*\/>/);
|
|
31
|
+
if (selfClosingMatch) {
|
|
32
|
+
pageMetaSource = selfClosingMatch[0];
|
|
33
|
+
newContent = content.replace(selfClosingMatch[0], "");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
}
|
|
25
37
|
const wrappedContent = `
|
|
26
38
|
${pageMetaSource}
|
|
27
39
|
<global-root-view>${newContent}</global-root-view>
|
|
28
40
|
`;
|
|
29
41
|
return {
|
|
30
|
-
code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart)
|
|
42
|
+
code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart),
|
|
43
|
+
map: {
|
|
44
|
+
version: 3,
|
|
45
|
+
sources: [],
|
|
46
|
+
names: [],
|
|
47
|
+
mappings: ""
|
|
48
|
+
}
|
|
31
49
|
};
|
|
32
50
|
}
|
|
33
51
|
export {
|
|
@@ -21,6 +21,14 @@ __export(root_exports, {
|
|
|
21
21
|
registerRootApp: () => registerRootApp
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(root_exports);
|
|
24
|
+
function createEmptySourcemap() {
|
|
25
|
+
return {
|
|
26
|
+
version: 3,
|
|
27
|
+
sources: [],
|
|
28
|
+
names: [],
|
|
29
|
+
mappings: ""
|
|
30
|
+
};
|
|
31
|
+
}
|
|
24
32
|
function registerRootApp(code, fileName = "App.root") {
|
|
25
33
|
const importCode = `import GlobalRootView from "./${fileName}.vue";`;
|
|
26
34
|
const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
|
|
@@ -33,10 +41,10 @@ function registerRootApp(code, fileName = "App.root") {
|
|
|
33
41
|
newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}
|
|
34
42
|
$2`);
|
|
35
43
|
}
|
|
36
|
-
return { code: newCode };
|
|
44
|
+
return { code: newCode, map: createEmptySourcemap() };
|
|
37
45
|
}
|
|
38
46
|
function rebuildRootApp(code) {
|
|
39
|
-
return { code };
|
|
47
|
+
return { code, map: createEmptySourcemap() };
|
|
40
48
|
}
|
|
41
49
|
// Annotate the CommonJS export names for ESM import in node:
|
|
42
50
|
0 && (module.exports = {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
function createEmptySourcemap() {
|
|
2
|
+
return {
|
|
3
|
+
version: 3,
|
|
4
|
+
sources: [],
|
|
5
|
+
names: [],
|
|
6
|
+
mappings: ""
|
|
7
|
+
};
|
|
8
|
+
}
|
|
1
9
|
function registerRootApp(code, fileName = "App.root") {
|
|
2
10
|
const importCode = `import GlobalRootView from "./${fileName}.vue";`;
|
|
3
11
|
const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
|
|
@@ -10,10 +18,10 @@ function registerRootApp(code, fileName = "App.root") {
|
|
|
10
18
|
newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}
|
|
11
19
|
$2`);
|
|
12
20
|
}
|
|
13
|
-
return { code: newCode };
|
|
21
|
+
return { code: newCode, map: createEmptySourcemap() };
|
|
14
22
|
}
|
|
15
23
|
function rebuildRootApp(code) {
|
|
16
|
-
return { code };
|
|
24
|
+
return { code, map: createEmptySourcemap() };
|
|
17
25
|
}
|
|
18
26
|
export {
|
|
19
27
|
rebuildRootApp,
|
|
@@ -46,7 +46,7 @@ async function parseSFC(code) {
|
|
|
46
46
|
const { parse } = await import("vue/compiler-sfc");
|
|
47
47
|
return parse(code, { pad: "space" }).descriptor;
|
|
48
48
|
} catch {
|
|
49
|
-
|
|
49
|
+
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
function stripJsonComments(str) {
|
|
@@ -6,7 +6,7 @@ async function parseSFC(code) {
|
|
|
6
6
|
const { parse } = await import("vue/compiler-sfc");
|
|
7
7
|
return parse(code, { pad: "space" }).descriptor;
|
|
8
8
|
} catch {
|
|
9
|
-
|
|
9
|
+
return null;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
function stripJsonComments(str) {
|
package/plugins/root/index.ts
CHANGED
|
@@ -1,123 +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 };
|
|
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 };
|
package/plugins/root/page.ts
CHANGED
|
@@ -1,54 +1,77 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
* 降级机制:当 vue/compiler-sfc 不可用(如 HBuilderX 项目)时,
|
|
15
|
+
* 使用正则匹配 <page-meta> 标签替代 AST 解析。
|
|
16
|
+
*/
|
|
17
|
+
export async function transformPage(code: string) {
|
|
18
|
+
const sfc = await parseSFC(code);
|
|
19
|
+
|
|
20
|
+
// 用正则定位 <template> 开头标签
|
|
21
|
+
const openMatch = code.match(/<template\b[^>]*>/);
|
|
22
|
+
if (!openMatch) return null;
|
|
23
|
+
|
|
24
|
+
// 使用 lastIndexOf 定位最后一个 </template>,即根 <template> 的闭合标签
|
|
25
|
+
// 避免匹配到模板内具名插槽 <template #slot> 的闭合标签
|
|
26
|
+
const closeTagStart = code.lastIndexOf('</template>');
|
|
27
|
+
if (closeTagStart === -1) return null;
|
|
28
|
+
|
|
29
|
+
const openTagEnd = openMatch.index! + openMatch[0].length;
|
|
30
|
+
const content = code.slice(openTagEnd, closeTagStart);
|
|
31
|
+
|
|
32
|
+
let pageMetaSource = '';
|
|
33
|
+
let newContent = content;
|
|
34
|
+
|
|
35
|
+
if (sfc?.template) {
|
|
36
|
+
// AST 模式:通过 vue/compiler-sfc 解析,精确定位 PageMeta 节点
|
|
37
|
+
const pageMetaNode = findNode(sfc, 'PageMeta');
|
|
38
|
+
if (pageMetaNode) {
|
|
39
|
+
pageMetaSource = pageMetaNode.loc.source;
|
|
40
|
+
const metaStart = pageMetaNode.loc.start.offset;
|
|
41
|
+
const metaEnd = pageMetaNode.loc.end.offset;
|
|
42
|
+
|
|
43
|
+
// pageMetaNode.loc 的 offset 是相对于整个文件的,转换为相对于 content 的位置
|
|
44
|
+
const metaStartInContent = metaStart - openTagEnd;
|
|
45
|
+
const metaEndInContent = metaEnd - openTagEnd;
|
|
46
|
+
|
|
47
|
+
newContent = content.slice(0, metaStartInContent) + content.slice(metaEndInContent);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// 降级模式:vue/compiler-sfc 不可用,用正则匹配 <page-meta> 标签
|
|
51
|
+
const pageMetaMatch = content.match(/<page-meta\b[^>]*>[\s\S]*?<\/page-meta>/);
|
|
52
|
+
if (pageMetaMatch) {
|
|
53
|
+
pageMetaSource = pageMetaMatch[0];
|
|
54
|
+
newContent = content.replace(pageMetaMatch[0], '');
|
|
55
|
+
} else {
|
|
56
|
+
// 也匹配自闭合的 <page-meta ... />
|
|
57
|
+
const selfClosingMatch = content.match(/<page-meta\b[^>]*\/>/);
|
|
58
|
+
if (selfClosingMatch) {
|
|
59
|
+
pageMetaSource = selfClosingMatch[0];
|
|
60
|
+
newContent = content.replace(selfClosingMatch[0], '');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// page-meta 放在包裹层外部,内容包裹在 <global-root-view> 中
|
|
66
|
+
const wrappedContent = `\n${pageMetaSource}\n<global-root-view>${newContent}</global-root-view>\n`;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
code: code.slice(0, openTagEnd) + wrappedContent + code.slice(closeTagStart),
|
|
70
|
+
map: {
|
|
71
|
+
version: 3 as const,
|
|
72
|
+
sources: [] as string[],
|
|
73
|
+
names: [] as string[],
|
|
74
|
+
mappings: ''
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/plugins/root/root.ts
CHANGED
|
@@ -1,35 +1,53 @@
|
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 创建一个最小的 sourcemap,用于满足 Vite/Rollup 的 sourcemap 要求。
|
|
3
|
+
*
|
|
4
|
+
* 这是一个空映射的 sourcemap(mappings 为空字符串),不提供精确的源码映射,
|
|
5
|
+
* 但可以避免 "Sourcemap is likely to be incorrect" 警告。
|
|
6
|
+
* 调试器会回退到显示转换后的代码,不影响功能。
|
|
7
|
+
*
|
|
8
|
+
* 纯 JS 实现,无需第三方依赖。
|
|
9
|
+
*/
|
|
10
|
+
function createEmptySourcemap() {
|
|
11
|
+
return {
|
|
12
|
+
version: 3 as const,
|
|
13
|
+
sources: [] as string[],
|
|
14
|
+
names: [] as string[],
|
|
15
|
+
mappings: ''
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 向 main.ts 注入 App.root.vue 的导入和全局注册
|
|
21
|
+
*
|
|
22
|
+
* 流程:
|
|
23
|
+
* 1. 在文件顶部插入 `import GlobalRootView from "./App.root.vue"`
|
|
24
|
+
* 2. 在 createSSRApp 之后插入 `app.component("global-root-view", GlobalRootView)`
|
|
25
|
+
*
|
|
26
|
+
* 注:GlobalRootView 仅为 main.ts 中的局部变量名,
|
|
27
|
+
* 实际组件就是 App.root.vue,页面通过 <global-root-view> 标签使用。
|
|
28
|
+
*/
|
|
29
|
+
export function registerRootApp(code: string, fileName: string = 'App.root') {
|
|
30
|
+
const importCode = `import GlobalRootView from "./${fileName}.vue";`;
|
|
31
|
+
const vueUseComponentCode = `app.component("global-root-view", GlobalRootView);`;
|
|
32
|
+
|
|
33
|
+
let newCode = importCode + '\n' + code;
|
|
34
|
+
|
|
35
|
+
// 在 createSSRApp 赋值语句之后插入组件注册代码
|
|
36
|
+
const ssrAppMatch = newCode.match(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/);
|
|
37
|
+
if (ssrAppMatch) {
|
|
38
|
+
newCode = newCode.replace(/(const\s+app\s*=\s*createSSRApp\([^)]+\);)/, `$1\n ${vueUseComponentCode}`);
|
|
39
|
+
} else {
|
|
40
|
+
// 兜底:在 return { 之前插入
|
|
41
|
+
newCode = newCode.replace(/(createApp[\s\S]*?)(return\s*\{)/, `$1${vueUseComponentCode}\n $2`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { code: newCode, map: createEmptySourcemap() };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* App.root.vue 模板中已使用 <slot /> 作为子内容插槽,
|
|
49
|
+
* 无需替换,直接透传。
|
|
50
|
+
*/
|
|
51
|
+
export function rebuildRootApp(code: string) {
|
|
52
|
+
return { code, map: createEmptySourcemap() };
|
|
53
|
+
}
|