vite-plugin-vue-transition-root-validator 0.0.1 → 0.0.3
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/README.md +11 -2
- package/dist/{client-C1tyP3h4.d.ts.map → client-DdpHavRX.d.ts.map} +1 -1
- package/dist/{client-C1tyP3h4.d.ts → client.d.ts} +16 -2
- package/dist/client.js +3 -3
- package/dist/client.js.map +1 -1
- package/dist/index-BGyd2pvz.d.ts.map +1 -0
- package/dist/index-BnGy14AF.d.cts.map +1 -0
- package/dist/index.cjs +13 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/package.json +17 -3
- package/dist/index-BIaiUtvr.d.cts.map +0 -1
- package/dist/index-BPjVwmbE.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
## 安装
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
|
|
7
|
+
pnpm i -D vite-plugin-vue-transition-root-validator
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## 使用
|
|
@@ -31,7 +31,7 @@ export default defineConfig({
|
|
|
31
31
|
```ts
|
|
32
32
|
import { createApp } from 'vue';
|
|
33
33
|
import App from './App.vue';
|
|
34
|
-
import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
34
|
+
import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
35
35
|
|
|
36
36
|
const app = createApp(App);
|
|
37
37
|
|
|
@@ -45,6 +45,15 @@ app.mount('#app');
|
|
|
45
45
|
|
|
46
46
|
> 说明:overlay 的标题(message header)与正文(stack)都会跟随此处的 `lang`。
|
|
47
47
|
|
|
48
|
+
## 常见问题
|
|
49
|
+
|
|
50
|
+
### TS 报错:找不到模块“virtual:vue-transition-root-validator”
|
|
51
|
+
|
|
52
|
+
如果在 `main.ts` 中引入时遇到类型报错,请在项目的 `src/vite-env.d.ts`(或 `env.d.ts`)中添加以下引用:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
/// <reference types="vite-plugin-vue-transition-root-validator/client" />
|
|
56
|
+
```
|
|
48
57
|
|
|
49
58
|
## 代码执行流程
|
|
50
59
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-
|
|
1
|
+
{"version":3,"file":"client-DdpHavRX.d.ts","names":[],"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":[],"mappings":";;;;ACoNqC,KDzMzB,IAAA,GCyMyB,IAAA,GAAA,IAAA;;;;ADzMrC;;KCEK,YAAA;;EAAA,IAAA,CAAA,EAEI,IAFJ;EAuMW;EAAqB,sBAAA,CAAA,EAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAArB,qBAAA,MAA2B,eAAc"}
|
|
@@ -25,7 +25,7 @@ type SetupOptions = {
|
|
|
25
25
|
* @example
|
|
26
26
|
* ```ts
|
|
27
27
|
* import { createApp } from 'vue';
|
|
28
|
-
* import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
28
|
+
* import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
29
29
|
* import App from './App.vue';
|
|
30
30
|
*
|
|
31
31
|
* const app = createApp(App);
|
|
@@ -39,4 +39,18 @@ type SetupOptions = {
|
|
|
39
39
|
declare function setupVueRootValidator(app: App, options?: SetupOptions): void;
|
|
40
40
|
//#endregion
|
|
41
41
|
export { setupVueRootValidator };
|
|
42
|
-
//# sourceMappingURL=client-
|
|
42
|
+
//# sourceMappingURL=client-DdpHavRX.d.ts.map
|
|
43
|
+
declare module 'virtual:vue-transition-root-validator' {
|
|
44
|
+
import type { App } from 'vue';
|
|
45
|
+
|
|
46
|
+
export type Lang = 'en' | 'zh';
|
|
47
|
+
|
|
48
|
+
export interface SetupOptions {
|
|
49
|
+
/** 界面语言,默认 'en' */
|
|
50
|
+
lang?: Lang;
|
|
51
|
+
/** 是否在检测到错误后自动禁用检测(避免重复报错),默认 false */
|
|
52
|
+
disableAfterFirstError?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function setupVueRootValidator(app: App, options?: SetupOptions): void;
|
|
56
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -75,13 +75,13 @@ function getViewUrlFromInstance(instance) {
|
|
|
75
75
|
* 检查插件是否已经安装
|
|
76
76
|
*/
|
|
77
77
|
function alreadyInstalled() {
|
|
78
|
-
return Boolean(globalThis["
|
|
78
|
+
return Boolean(globalThis["__VITE_PLUGIN_VUE_TRANSITION_ROOT_VALIDATOR_INSTALLED__"]);
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* 标记插件已安装
|
|
82
82
|
*/
|
|
83
83
|
function markInstalled() {
|
|
84
|
-
globalThis["
|
|
84
|
+
globalThis["__VITE_PLUGIN_VUE_TRANSITION_ROOT_VALIDATOR_INSTALLED__"] = true;
|
|
85
85
|
}
|
|
86
86
|
const pendingPayloads = /* @__PURE__ */ new Map();
|
|
87
87
|
let listenersBound = false;
|
|
@@ -163,7 +163,7 @@ function resendLog(msg, instance, trace) {
|
|
|
163
163
|
* @example
|
|
164
164
|
* ```ts
|
|
165
165
|
* import { createApp } from 'vue';
|
|
166
|
-
* import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
166
|
+
* import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
167
167
|
* import App from './App.vue';
|
|
168
168
|
*
|
|
169
169
|
* const app = createApp(App);
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["title","lines","meta"],"sources":["../src/i18n.ts","../src/client.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import type { App, AppConfig, ComponentPublicInstance } from 'vue';\nimport type { Lang } from './types.ts';\nimport { formatTransitionRootMessage } from './i18n.ts';\n\n/**\n * Vue Transition 警告关键字\n * 用于识别 Vue 运行时发出的 Transition 多根节点警告\n */\nconst VUE_TRANSITION_WARN = 'Component inside <Transition> renders non-element root node that cannot be animated.';\n\n/**\n * 客户端配置选项\n */\ntype SetupOptions = {\n /** 界面语言,默认 'en' */\n lang?: Lang;\n /** 是否在检测到错误后自动禁用检测(避免重复报错),默认 false */\n disableAfterFirstError?: boolean;\n};\n\n/**\n * HMR 发送的消息载荷\n */\ntype Payload = {\n key: string;\n message: string;\n lang: Lang;\n};\n\n/**\n * 从 Vue 警告信息中提取组件名称\n * 示例: \"at <Index onVnodeUnmounted=... key=\"/test\" ... >\"\n */\nfunction extractComponentName(text: string): string | undefined {\n const m = text.match(/at <([^\\s>]+)/);\n return m?.[1];\n}\n\n/**\n * 从 Vue 警告信息中提取路由 key\n * 示例: key=\"/test\"\n */\nfunction extractRouteKey(text: string): string | undefined {\n // eslint-disable-next-line no-useless-escape\n const m = text.match(/key=\\\"([^\\\"]+)\\\"/);\n return m?.[1];\n}\n\n/**\n * 获取组件实例可能对应的文件路径\n */\nfunction guessViewFileFromInstance(instance: ComponentPublicInstance | null | undefined): string | undefined {\n if (!instance) return undefined;\n\n // eslint-disable-next-line dot-notation\n return (instance.$options as any)['__file'];\n}\n\n/**\n * 获取组件实例可能对应的文件路径\n */\nfunction getViewUrlFromInstance(instance: ComponentPublicInstance | null | undefined): string | undefined {\n if (!instance) return undefined;\n\n return instance.$el?.baseURI;\n}\n\n/**\n * 检查插件是否已经安装\n */\nfunction alreadyInstalled(): boolean {\n // eslint-disable-next-line dot-notation\n return Boolean((globalThis as any)['__VITE_PLUGIN_VUE_ROOT_VALIDATOR_INSTALLED__']);\n}\n\n/**\n * 标记插件已安装\n */\nfunction markInstalled() {\n // eslint-disable-next-line dot-notation\n (globalThis as any)['__VITE_PLUGIN_VUE_ROOT_VALIDATOR_INSTALLED__'] = true;\n}\n\n/**\n * 通过 HMR WebSocket 向 Vite 服务器发送消息\n */\ntype HotLike = {\n send?: (event: string, payload: unknown) => void;\n on?: (event: string, cb: (data: any) => void) => void;\n};\n\nconst pendingPayloads = new Map<string, Payload>();\nlet listenersBound = false;\nlet retryTimer: number | null = null;\nlet retryDelayMs = 200;\n\nfunction getHot(): HotLike | undefined {\n return (import.meta as any).hot as HotLike | undefined;\n}\n\nfunction trySendNow(payload: Payload): boolean {\n const hot = getHot();\n if (!hot?.send) return false;\n\n try {\n hot.send('vite-plugin-vue-transition-root-validator:vue-warn', payload);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction scheduleRetry() {\n if (retryTimer !== null) return;\n\n // 轻量兜底:如果错过了 vite:ws:connect 事件,也会在短时间内尝试重发。\n retryTimer = globalThis.setTimeout(() => {\n retryTimer = null;\n flushPendingPayloads();\n\n // 如果还有积压,继续重试(指数退避,避免持续高频)\n if (pendingPayloads.size) {\n retryDelayMs = Math.min(retryDelayMs * 2, 2000);\n scheduleRetry();\n }\n }, retryDelayMs) as unknown as number;\n}\n\nfunction flushPendingPayloads() {\n if (!pendingPayloads.size) return;\n\n // fire-and-forget:是否送达由服务端 ACK 决定;未 ACK 的会继续重试\n for (const p of pendingPayloads.values()) {\n trySendNow(p);\n }\n}\n\nfunction bindHmrListenersOnce() {\n if (listenersBound) return;\n listenersBound = true;\n\n const hot = getHot();\n if (!hot?.on) return;\n\n // 连接建立后立刻 flush 一次,减少首屏丢消息的概率\n hot.on('vite:ws:connect', () => {\n retryDelayMs = 200;\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n flushPendingPayloads();\n });\n\n // 服务端 ACK:用于清理重试队列,避免重复发送\n hot.on('vite-plugin-vue-transition-root-validator:ack', (data: any) => {\n const key = data?.key;\n if (!key) return;\n\n pendingPayloads.delete(key);\n if (!pendingPayloads.size) {\n retryDelayMs = 200;\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n }\n });\n}\n\nfunction send(payload: Payload) {\n // 仅在开发环境且 HMR 启用时工作\n const hot = getHot();\n if (!hot?.send) return;\n\n bindHmrListenersOnce();\n\n pendingPayloads.set(payload.key, payload);\n trySendNow(payload);\n scheduleRetry();\n}\n\nlet originalWarnHandler: null | AppConfig['warnHandler'] = null;\n\nfunction resendLog(msg: string, instance: ComponentPublicInstance | null, trace: string) {\n if (originalWarnHandler) {\n originalWarnHandler(msg, instance, trace);\n }\n}\n\n/**\n * 设置 Vue Root Validator\n *\n * 在 Vue 应用上注册 warnHandler 来捕获 Transition 多根节点警告\n *\n * @param app - Vue 应用实例\n * @param options - 配置选项\n *\n * @example\n * ```ts\n * import { createApp } from 'vue';\n * import { setupVueRootValidator } from 'virtual:vue-root-validator';\n * import App from './App.vue';\n *\n * const app = createApp(App);\n *\n * // 在挂载前设置验证器\n * setupVueRootValidator(app, { lang: 'zh' });\n *\n * app.mount('#app');\n * ```\n */\nexport function setupVueRootValidator(app: App, options: SetupOptions = {}) {\n // 防止重复安装\n if (alreadyInstalled()) {\n return;\n }\n markInstalled();\n\n const lang: Lang = options.lang ?? 'en';\n const disableAfterFirstError = options.disableAfterFirstError ?? false;\n\n // 尽早绑定 HMR 连接监听,避免首次触发 warning 时 ws 尚未 ready。\n bindHmrListenersOnce();\n\n // 保存原始的 warnHandler(如果有)\n originalWarnHandler = app.config.warnHandler;\n\n // 用于防抖,避免同一错误重复发送\n let lastSentAt = 0;\n let lastSentKey = '';\n let errorSent = false;\n\n /**\n * 自定义 Vue 警告处理器\n *\n * @param msg - 警告消息\n * @param instance - 组件实例\n * @param trace - 组件追踪栈\n */\n app.config.warnHandler = (msg: string, instance: ComponentPublicInstance | null, trace: string) => {\n resendLog(msg, instance, trace);\n // 如果已经发送过错误且配置为只发送一次,则跳过\n if (disableAfterFirstError && errorSent) {\n return;\n }\n\n // 检查是否是 Transition 多根节点警告\n const matched = msg.includes(VUE_TRANSITION_WARN);\n\n if (!matched) {\n // 不是目标警告\n return;\n }\n\n // 防抖处理:避免短时间内重复发送同一个错误\n const now = Date.now();\n const text = msg + trace;\n const key = text.slice(0, 400);\n\n if (now - lastSentAt > 500 || key !== lastSentKey) {\n lastSentAt = now;\n lastSentKey = key;\n\n // 从 trace 中提取信息\n const routeKey = extractRouteKey(trace);\n const component = extractComponentName(trace);\n const file = guessViewFileFromInstance(instance);\n const url = getViewUrlFromInstance(instance);\n\n // 格式化错误消息\n const message = formatTransitionRootMessage(lang, {\n url,\n file,\n routeKey,\n component\n });\n\n // 发送到 Vite 服务器\n send({ key: message.slice(0, 800), message, lang });\n\n errorSent = true;\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["title","lines","meta"],"sources":["../src/i18n.ts","../src/client.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import type { App, AppConfig, ComponentPublicInstance } from 'vue';\nimport type { Lang } from './types.ts';\nimport { formatTransitionRootMessage } from './i18n.ts';\n\n/**\n * Vue Transition 警告关键字\n * 用于识别 Vue 运行时发出的 Transition 多根节点警告\n */\nconst VUE_TRANSITION_WARN = 'Component inside <Transition> renders non-element root node that cannot be animated.';\n\n/**\n * 客户端配置选项\n */\ntype SetupOptions = {\n /** 界面语言,默认 'en' */\n lang?: Lang;\n /** 是否在检测到错误后自动禁用检测(避免重复报错),默认 false */\n disableAfterFirstError?: boolean;\n};\n\n/**\n * HMR 发送的消息载荷\n */\ntype Payload = {\n key: string;\n message: string;\n lang: Lang;\n};\n\n/**\n * 从 Vue 警告信息中提取组件名称\n * 示例: \"at <Index onVnodeUnmounted=... key=\"/test\" ... >\"\n */\nfunction extractComponentName(text: string): string | undefined {\n const m = text.match(/at <([^\\s>]+)/);\n return m?.[1];\n}\n\n/**\n * 从 Vue 警告信息中提取路由 key\n * 示例: key=\"/test\"\n */\nfunction extractRouteKey(text: string): string | undefined {\n // eslint-disable-next-line no-useless-escape\n const m = text.match(/key=\\\"([^\\\"]+)\\\"/);\n return m?.[1];\n}\n\n/**\n * 获取组件实例可能对应的文件路径\n */\nfunction guessViewFileFromInstance(instance: ComponentPublicInstance | null | undefined): string | undefined {\n if (!instance) return undefined;\n\n // eslint-disable-next-line dot-notation\n return (instance.$options as any)['__file'];\n}\n\n/**\n * 获取组件实例可能对应的文件路径\n */\nfunction getViewUrlFromInstance(instance: ComponentPublicInstance | null | undefined): string | undefined {\n if (!instance) return undefined;\n\n return instance.$el?.baseURI;\n}\n\n/**\n * 检查插件是否已经安装\n */\nfunction alreadyInstalled(): boolean {\n // eslint-disable-next-line dot-notation\n return Boolean((globalThis as any)['__VITE_PLUGIN_VUE_TRANSITION_ROOT_VALIDATOR_INSTALLED__']);\n}\n\n/**\n * 标记插件已安装\n */\nfunction markInstalled() {\n // eslint-disable-next-line dot-notation\n (globalThis as any)['__VITE_PLUGIN_VUE_TRANSITION_ROOT_VALIDATOR_INSTALLED__'] = true;\n}\n\n/**\n * 通过 HMR WebSocket 向 Vite 服务器发送消息\n */\ntype HotLike = {\n send?: (event: string, payload: unknown) => void;\n on?: (event: string, cb: (data: any) => void) => void;\n};\n\nconst pendingPayloads = new Map<string, Payload>();\nlet listenersBound = false;\nlet retryTimer: number | null = null;\nlet retryDelayMs = 200;\n\nfunction getHot(): HotLike | undefined {\n return (import.meta as any).hot as HotLike | undefined;\n}\n\nfunction trySendNow(payload: Payload): boolean {\n const hot = getHot();\n if (!hot?.send) return false;\n\n try {\n hot.send('vite-plugin-vue-transition-root-validator:vue-warn', payload);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction scheduleRetry() {\n if (retryTimer !== null) return;\n\n // 轻量兜底:如果错过了 vite:ws:connect 事件,也会在短时间内尝试重发。\n retryTimer = globalThis.setTimeout(() => {\n retryTimer = null;\n flushPendingPayloads();\n\n // 如果还有积压,继续重试(指数退避,避免持续高频)\n if (pendingPayloads.size) {\n retryDelayMs = Math.min(retryDelayMs * 2, 2000);\n scheduleRetry();\n }\n }, retryDelayMs) as unknown as number;\n}\n\nfunction flushPendingPayloads() {\n if (!pendingPayloads.size) return;\n\n // fire-and-forget:是否送达由服务端 ACK 决定;未 ACK 的会继续重试\n for (const p of pendingPayloads.values()) {\n trySendNow(p);\n }\n}\n\nfunction bindHmrListenersOnce() {\n if (listenersBound) return;\n listenersBound = true;\n\n const hot = getHot();\n if (!hot?.on) return;\n\n // 连接建立后立刻 flush 一次,减少首屏丢消息的概率\n hot.on('vite:ws:connect', () => {\n retryDelayMs = 200;\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n flushPendingPayloads();\n });\n\n // 服务端 ACK:用于清理重试队列,避免重复发送\n hot.on('vite-plugin-vue-transition-root-validator:ack', (data: any) => {\n const key = data?.key;\n if (!key) return;\n\n pendingPayloads.delete(key);\n if (!pendingPayloads.size) {\n retryDelayMs = 200;\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n }\n });\n}\n\nfunction send(payload: Payload) {\n // 仅在开发环境且 HMR 启用时工作\n const hot = getHot();\n if (!hot?.send) return;\n\n bindHmrListenersOnce();\n\n pendingPayloads.set(payload.key, payload);\n trySendNow(payload);\n scheduleRetry();\n}\n\nlet originalWarnHandler: null | AppConfig['warnHandler'] = null;\n\nfunction resendLog(msg: string, instance: ComponentPublicInstance | null, trace: string) {\n if (originalWarnHandler) {\n originalWarnHandler(msg, instance, trace);\n }\n}\n\n/**\n * 设置 Vue Root Validator\n *\n * 在 Vue 应用上注册 warnHandler 来捕获 Transition 多根节点警告\n *\n * @param app - Vue 应用实例\n * @param options - 配置选项\n *\n * @example\n * ```ts\n * import { createApp } from 'vue';\n * import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n * import App from './App.vue';\n *\n * const app = createApp(App);\n *\n * // 在挂载前设置验证器\n * setupVueRootValidator(app, { lang: 'zh' });\n *\n * app.mount('#app');\n * ```\n */\nexport function setupVueRootValidator(app: App, options: SetupOptions = {}) {\n // 防止重复安装\n if (alreadyInstalled()) {\n return;\n }\n markInstalled();\n\n const lang: Lang = options.lang ?? 'en';\n const disableAfterFirstError = options.disableAfterFirstError ?? false;\n\n // 尽早绑定 HMR 连接监听,避免首次触发 warning 时 ws 尚未 ready。\n bindHmrListenersOnce();\n\n // 保存原始的 warnHandler(如果有)\n originalWarnHandler = app.config.warnHandler;\n\n // 用于防抖,避免同一错误重复发送\n let lastSentAt = 0;\n let lastSentKey = '';\n let errorSent = false;\n\n /**\n * 自定义 Vue 警告处理器\n *\n * @param msg - 警告消息\n * @param instance - 组件实例\n * @param trace - 组件追踪栈\n */\n app.config.warnHandler = (msg: string, instance: ComponentPublicInstance | null, trace: string) => {\n resendLog(msg, instance, trace);\n // 如果已经发送过错误且配置为只发送一次,则跳过\n if (disableAfterFirstError && errorSent) {\n return;\n }\n\n // 检查是否是 Transition 多根节点警告\n const matched = msg.includes(VUE_TRANSITION_WARN);\n\n if (!matched) {\n // 不是目标警告\n return;\n }\n\n // 防抖处理:避免短时间内重复发送同一个错误\n const now = Date.now();\n const text = msg + trace;\n const key = text.slice(0, 400);\n\n if (now - lastSentAt > 500 || key !== lastSentKey) {\n lastSentAt = now;\n lastSentKey = key;\n\n // 从 trace 中提取信息\n const routeKey = extractRouteKey(trace);\n const component = extractComponentName(trace);\n const file = guessViewFileFromInstance(instance);\n const url = getViewUrlFromInstance(instance);\n\n // 格式化错误消息\n const message = formatTransitionRootMessage(lang, {\n url,\n file,\n routeKey,\n component\n });\n\n // 发送到 Vite 服务器\n send({ key: message.slice(0, 800), message, lang });\n\n errorSent = true;\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BGyd2pvz.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;AAQkB;AAuBK,KAvBlB,aAAA,GA8EmB;EAAoC,EAAA,EAAA;IAUjC,IAAA,EAAA,CAAA,OAAA,EAAA;MAQC,IAAA,EAAA,OAAA;MAAa,GAAA,EAAA;;;;;;;;;;;;;;;;;KAzEpC,kBAAA;;;;;;;;;;;;;;;;;;;;;;iBAuDmB,oCAAA,CAAA;;;;;;yBAUG;;;;;0BAQC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BnGy14AF.d.cts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;AAQkB;AAuBK,KAvBlB,aAAA,GA8EmB;EAAoC,EAAA,EAAA;IAUjC,IAAA,EAAA,CAAA,OAAA,EAAA;MAQC,IAAA,EAAA,OAAA;MAAa,GAAA,EAAA;;;;;;;;;;;;;;;;;KAzEpC,kBAAA;;;;;;;;;;;;;;;;;;;;;;iBAuDmB,oCAAA,CAAA;;;;;;yBAUG;;;;;0BAQC"}
|
package/dist/index.cjs
CHANGED
|
@@ -9,14 +9,14 @@ const translations = {
|
|
|
9
9
|
如何在项目中启用此插件:
|
|
10
10
|
|
|
11
11
|
1. 在 vite.config.ts 中添加插件:
|
|
12
|
-
import
|
|
12
|
+
import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
13
13
|
|
|
14
14
|
plugins: [
|
|
15
|
-
|
|
15
|
+
vitePluginVueTransitionRootValidator()
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
2. 在 src/main.ts 中初始化:
|
|
19
|
-
import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
19
|
+
import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
20
20
|
|
|
21
21
|
const app = createApp(App);
|
|
22
22
|
setupVueRootValidator(app, { lang: 'zh' });
|
|
@@ -29,14 +29,14 @@ const translations = {
|
|
|
29
29
|
How to enable this plugin in your project:
|
|
30
30
|
|
|
31
31
|
1. Add plugin in vite.config.ts:
|
|
32
|
-
import
|
|
32
|
+
import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
33
33
|
|
|
34
34
|
plugins: [
|
|
35
|
-
|
|
35
|
+
vitePluginVueTransitionRootValidator()
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
2. Initialize in src/main.ts:
|
|
39
|
-
import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
39
|
+
import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
40
40
|
|
|
41
41
|
const app = createApp(App);
|
|
42
42
|
setupVueRootValidator(app, { lang: 'en' });
|
|
@@ -78,16 +78,16 @@ function sendErrorOverlay(args) {
|
|
|
78
78
|
* @example
|
|
79
79
|
* ```ts
|
|
80
80
|
* // vite.config.ts
|
|
81
|
-
* import
|
|
81
|
+
* import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
82
82
|
*
|
|
83
83
|
* export default defineConfig({
|
|
84
84
|
* plugins: [
|
|
85
|
-
*
|
|
85
|
+
* vitePluginVueTransitionRootValidator()
|
|
86
86
|
* ]
|
|
87
87
|
* });
|
|
88
88
|
* ```
|
|
89
89
|
*/
|
|
90
|
-
function
|
|
90
|
+
function vitePluginVueTransitionRootValidator() {
|
|
91
91
|
let resolved;
|
|
92
92
|
return {
|
|
93
93
|
name: "vite-plugin-vue-transition-root-validator",
|
|
@@ -117,16 +117,16 @@ function vitePluginVueRootValidator() {
|
|
|
117
117
|
});
|
|
118
118
|
},
|
|
119
119
|
resolveId(id) {
|
|
120
|
-
if (id === "virtual:vue-root-validator") return "\0virtual:vue-root-validator";
|
|
120
|
+
if (id === "virtual:vue-transition-root-validator") return "\0virtual:vue-transition-root-validator";
|
|
121
121
|
return null;
|
|
122
122
|
},
|
|
123
123
|
load(id) {
|
|
124
|
-
if (id === "\0virtual:vue-root-validator") {
|
|
124
|
+
if (id === "\0virtual:vue-transition-root-validator") {
|
|
125
125
|
const clientEntryTs = (0, node_url.fileURLToPath)(new URL("./client.ts", require("url").pathToFileURL(__filename).href));
|
|
126
126
|
const clientEntryJs = (0, node_url.fileURLToPath)(new URL("./client.js", require("url").pathToFileURL(__filename).href));
|
|
127
127
|
const clientUrl = `/@fs/${((0, node_fs.existsSync)(clientEntryTs) ? clientEntryTs : clientEntryJs).replace(/\\/g, "/")}`;
|
|
128
128
|
return `
|
|
129
|
-
// 虚拟模块:vue-root-validator
|
|
129
|
+
// 虚拟模块:vue-transition-root-validator
|
|
130
130
|
// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成
|
|
131
131
|
|
|
132
132
|
import { setupVueRootValidator } from ${JSON.stringify(clientUrl)};
|
|
@@ -141,5 +141,5 @@ export { setupVueRootValidator };
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
//#endregion
|
|
144
|
-
module.exports =
|
|
144
|
+
module.exports = vitePluginVueTransitionRootValidator;
|
|
145
145
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/i18n.ts","../src/index.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport type { Lang } from './types.ts';\nimport { getMessageHeader } from './i18n.ts';\n\n/**\n * Vite DevServer 类型(精简版)\n */\ntype DevServerLike = {\n ws: {\n send: (payload: { type: 'error'; err: { message: string; stack?: string } }) => void;\n on: (event: string, listener: (payload: any, client: any) => void) => void;\n };\n config: {\n logger: {\n warn: (msg: string) => void;\n info: (msg: string) => void;\n };\n };\n};\n\ntype DevClientLike = {\n send?: {\n (payload: { type: 'error'; err: { message: string; stack?: string } }): void;\n (event: string, payload?: any): void;\n };\n};\n\n/**\n * Vite ResolvedConfig 类型(精简版)\n */\ntype ResolvedConfigLike = {\n command: 'serve' | 'build' | string;\n};\n\n/**\n * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)\n */\nfunction sendErrorOverlay(args: { server: DevServerLike; message: string; lang: Lang; client?: DevClientLike }) {\n const { server, message, lang, client } = args;\n const payload = {\n type: 'error',\n err: {\n message: getMessageHeader(lang),\n stack: message\n }\n } as const;\n\n if (client?.send) {\n client.send(payload);\n return;\n }\n\n server.ws.send(payload);\n}\n\n/**\n * 客户端上报的消息载荷\n */\ntype ClientReportPayload = {\n message: string;\n /** 用于 ACK 去重/确认(客户端可不传,服务端会回退使用 message 截断值) */\n key?: string;\n /** 客户端运行时语言(推荐从 main.ts 传入并上报),用于决定 overlay header 语言 */\n lang?: Lang;\n};\n\n/**\n * Vite 插件:Vue Root Validator\n *\n * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题\n *\n * @returns Vite 插件对象\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n *\n * export default defineConfig({\n * plugins: [\n * vitePluginVueRootValidator()\n * ]\n * });\n * ```\n */\nexport default function vitePluginVueRootValidator() {\n let resolved: ResolvedConfigLike;\n\n return {\n name: 'vite-plugin-vue-transition-root-validator',\n apply: 'serve' as const, // 仅在开发环境应用\n\n /**\n * 保存解析后的配置\n */\n configResolved(config: ResolvedConfigLike) {\n resolved = config;\n },\n\n /**\n * 配置开发服务器\n * 监听客户端上报的警告消息,并通过 error overlay 显示\n */\n configureServer(server: DevServerLike) {\n if (resolved.command !== 'serve') return;\n\n // 用于去重,避免同一客户端重复发送相同消息\n const lastByClient = new WeakMap<object, string>();\n\n // 监听客户端上报的警告\n server.ws.on('vite-plugin-vue-transition-root-validator:vue-warn', (payload: ClientReportPayload, client: object) => {\n if (!payload?.message) return;\n\n // 去重处理\n const key = payload.key ?? payload.message.slice(0, 800);\n const prev = lastByClient.get(client as unknown as object);\n if (prev === key) {\n // 仍然回 ACK,避免客户端重试积压\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n return;\n }\n lastByClient.set(client as unknown as object, key);\n\n const effectiveLang: Lang = payload.lang ?? 'en';\n\n // 发送错误覆盖层\n sendErrorOverlay({\n server,\n message: payload.message,\n lang: effectiveLang,\n client: client as any as DevClientLike\n });\n\n // 回 ACK,通知客户端该消息已被处理(用于清理重试队列)\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n });\n },\n\n /**\n * 解析虚拟模块\n * 处理 'virtual:vue-root-validator' 模块的导入\n */\n resolveId(id: string) {\n if (id === 'virtual:vue-root-validator') {\n // 返回一个虚拟模块 ID,加上 \\0 前缀表示这是一个虚拟模块\n return '\\0virtual:vue-root-validator';\n }\n return null;\n },\n\n /**\n * 加载虚拟模块\n * 返回虚拟模块的代码内容\n */\n load(id: string) {\n if (id === '\\0virtual:vue-root-validator') {\n const clientEntryTs = fileURLToPath(new URL('./client.ts', import.meta.url));\n const clientEntryJs = fileURLToPath(new URL('./client.js', import.meta.url));\n const clientEntry = existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs;\n const clientUrl = `/@fs/${clientEntry.replace(/\\\\/g, '/')}`;\n\n // 返回虚拟模块代码:重新导出 client.ts 的函数\n return `\n// 虚拟模块:vue-root-validator\n// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成\n\nimport { setupVueRootValidator } from ${JSON.stringify(clientUrl)};\n\n// 重新导出函数\nexport { setupVueRootValidator };\n`;\n }\n return null;\n }\n };\n}\n\n\n"],"mappings":";;;;AASA,MAAM,eAAe;CACnB,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACD,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACF;AAiED,SAAgB,iBAAiB,MAAoB;AACnD,QAAO,aAAa,MAAM;;;;;;;;AC9E5B,SAAS,iBAAiB,MAAsF;CAC9G,MAAM,EAAE,QAAQ,SAAS,MAAM,WAAW;CAC1C,MAAM,UAAU;EACd,MAAM;EACN,KAAK;GACH,SAAS,iBAAiB,KAAK;GAC/B,OAAO;GACR;EACF;AAED,KAAI,QAAQ,MAAM;AAChB,SAAO,KAAK,QAAQ;AACpB;;AAGF,QAAO,GAAG,KAAK,QAAQ;;;;;;;;;;;;;;;;;;;;;AAiCzB,SAAwB,6BAA6B;CACnD,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAKP,eAAe,QAA4B;AACzC,cAAW;;EAOb,gBAAgB,QAAuB;AACrC,OAAI,SAAS,YAAY,QAAS;GAGlC,MAAM,+BAAe,IAAI,SAAyB;AAGlD,UAAO,GAAG,GAAG,uDAAuD,SAA8B,WAAmB;AACnH,QAAI,CAAC,SAAS,QAAS;IAGvB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAExD,QADa,aAAa,IAAI,OAA4B,KAC7C,KAAK;AAEhB,KAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;AAClG;;AAEF,iBAAa,IAAI,QAA6B,IAAI;IAElD,MAAM,gBAAsB,QAAQ,QAAQ;AAG5C,qBAAiB;KACf;KACA,SAAS,QAAQ;KACjB,MAAM;KACE;KACT,CAAC;AAGF,IAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;KAClG;;EAOJ,UAAU,IAAY;AACpB,OAAI,OAAO,6BAET,QAAO;AAET,UAAO;;EAOT,KAAK,IAAY;AACf,OAAI,OAAO,gCAAgC;IACzC,MAAM,4CAA8B,IAAI,IAAI,6DAA+B,CAAC;IAC5E,MAAM,4CAA8B,IAAI,IAAI,6DAA+B,CAAC;IAE5E,MAAM,YAAY,iCADa,cAAc,GAAG,gBAAgB,eAC1B,QAAQ,OAAO,IAAI;AAGzD,WAAO;;;;wCAIyB,KAAK,UAAU,UAAU,CAAC;;;;;;AAM5D,UAAO;;EAEV"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/i18n.ts","../src/index.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport type { Lang } from './types.ts';\nimport { getMessageHeader } from './i18n.ts';\n\n/**\n * Vite DevServer 类型(精简版)\n */\ntype DevServerLike = {\n ws: {\n send: (payload: { type: 'error'; err: { message: string; stack?: string } }) => void;\n on: (event: string, listener: (payload: any, client: any) => void) => void;\n };\n config: {\n logger: {\n warn: (msg: string) => void;\n info: (msg: string) => void;\n };\n };\n};\n\ntype DevClientLike = {\n send?: {\n (payload: { type: 'error'; err: { message: string; stack?: string } }): void;\n (event: string, payload?: any): void;\n };\n};\n\n/**\n * Vite ResolvedConfig 类型(精简版)\n */\ntype ResolvedConfigLike = {\n command: 'serve' | 'build' | string;\n};\n\n/**\n * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)\n */\nfunction sendErrorOverlay(args: { server: DevServerLike; message: string; lang: Lang; client?: DevClientLike }) {\n const { server, message, lang, client } = args;\n const payload = {\n type: 'error',\n err: {\n message: getMessageHeader(lang),\n stack: message\n }\n } as const;\n\n if (client?.send) {\n client.send(payload);\n return;\n }\n\n server.ws.send(payload);\n}\n\n/**\n * 客户端上报的消息载荷\n */\ntype ClientReportPayload = {\n message: string;\n /** 用于 ACK 去重/确认(客户端可不传,服务端会回退使用 message 截断值) */\n key?: string;\n /** 客户端运行时语言(推荐从 main.ts 传入并上报),用于决定 overlay header 语言 */\n lang?: Lang;\n};\n\n/**\n * Vite 插件:Vue Root Validator\n *\n * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题\n *\n * @returns Vite 插件对象\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n *\n * export default defineConfig({\n * plugins: [\n * vitePluginVueTransitionRootValidator()\n * ]\n * });\n * ```\n */\nexport default function vitePluginVueTransitionRootValidator() {\n let resolved: ResolvedConfigLike;\n\n return {\n name: 'vite-plugin-vue-transition-root-validator',\n apply: 'serve' as const, // 仅在开发环境应用\n\n /**\n * 保存解析后的配置\n */\n configResolved(config: ResolvedConfigLike) {\n resolved = config;\n },\n\n /**\n * 配置开发服务器\n * 监听客户端上报的警告消息,并通过 error overlay 显示\n */\n configureServer(server: DevServerLike) {\n if (resolved.command !== 'serve') return;\n\n // 用于去重,避免同一客户端重复发送相同消息\n const lastByClient = new WeakMap<object, string>();\n\n // 监听客户端上报的警告\n server.ws.on('vite-plugin-vue-transition-root-validator:vue-warn', (payload: ClientReportPayload, client: object) => {\n if (!payload?.message) return;\n\n // 去重处理\n const key = payload.key ?? payload.message.slice(0, 800);\n const prev = lastByClient.get(client as unknown as object);\n if (prev === key) {\n // 仍然回 ACK,避免客户端重试积压\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n return;\n }\n lastByClient.set(client as unknown as object, key);\n\n const effectiveLang: Lang = payload.lang ?? 'en';\n\n // 发送错误覆盖层\n sendErrorOverlay({\n server,\n message: payload.message,\n lang: effectiveLang,\n client: client as any as DevClientLike\n });\n\n // 回 ACK,通知客户端该消息已被处理(用于清理重试队列)\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n });\n },\n\n /**\n * 解析虚拟模块\n * 处理 'virtual:vue-transition-root-validator' 模块的导入\n */\n resolveId(id: string) {\n if (id === 'virtual:vue-transition-root-validator') {\n // 返回一个虚拟模块 ID,加上 \\0 前缀表示这是一个虚拟模块\n return '\\0virtual:vue-transition-root-validator';\n }\n return null;\n },\n\n /**\n * 加载虚拟模块\n * 返回虚拟模块的代码内容\n */\n load(id: string) {\n if (id === '\\0virtual:vue-transition-root-validator') {\n const clientEntryTs = fileURLToPath(new URL('./client.ts', import.meta.url));\n const clientEntryJs = fileURLToPath(new URL('./client.js', import.meta.url));\n const clientEntry = existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs;\n const clientUrl = `/@fs/${clientEntry.replace(/\\\\/g, '/')}`;\n\n // 返回虚拟模块代码:重新导出 client.ts 的函数\n return `\n// 虚拟模块:vue-transition-root-validator\n// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成\n\nimport { setupVueRootValidator } from ${JSON.stringify(clientUrl)};\n\n// 重新导出函数\nexport { setupVueRootValidator };\n`;\n }\n return null;\n }\n };\n}\n\n\n"],"mappings":";;;;AASA,MAAM,eAAe;CACnB,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACD,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACF;AAiED,SAAgB,iBAAiB,MAAoB;AACnD,QAAO,aAAa,MAAM;;;;;;;;AC9E5B,SAAS,iBAAiB,MAAsF;CAC9G,MAAM,EAAE,QAAQ,SAAS,MAAM,WAAW;CAC1C,MAAM,UAAU;EACd,MAAM;EACN,KAAK;GACH,SAAS,iBAAiB,KAAK;GAC/B,OAAO;GACR;EACF;AAED,KAAI,QAAQ,MAAM;AAChB,SAAO,KAAK,QAAQ;AACpB;;AAGF,QAAO,GAAG,KAAK,QAAQ;;;;;;;;;;;;;;;;;;;;;AAiCzB,SAAwB,uCAAuC;CAC7D,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAKP,eAAe,QAA4B;AACzC,cAAW;;EAOb,gBAAgB,QAAuB;AACrC,OAAI,SAAS,YAAY,QAAS;GAGlC,MAAM,+BAAe,IAAI,SAAyB;AAGlD,UAAO,GAAG,GAAG,uDAAuD,SAA8B,WAAmB;AACnH,QAAI,CAAC,SAAS,QAAS;IAGvB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAExD,QADa,aAAa,IAAI,OAA4B,KAC7C,KAAK;AAEhB,KAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;AAClG;;AAEF,iBAAa,IAAI,QAA6B,IAAI;IAElD,MAAM,gBAAsB,QAAQ,QAAQ;AAG5C,qBAAiB;KACf;KACA,SAAS,QAAQ;KACjB,MAAM;KACE;KACT,CAAC;AAGF,IAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;KAClG;;EAOJ,UAAU,IAAY;AACpB,OAAI,OAAO,wCAET,QAAO;AAET,UAAO;;EAOT,KAAK,IAAY;AACf,OAAI,OAAO,2CAA2C;IACpD,MAAM,4CAA8B,IAAI,IAAI,6DAA+B,CAAC;IAC5E,MAAM,4CAA8B,IAAI,IAAI,6DAA+B,CAAC;IAE5E,MAAM,YAAY,iCADa,cAAc,GAAG,gBAAgB,eAC1B,QAAQ,OAAO,IAAI;AAGzD,WAAO;;;;wCAIyB,KAAK,UAAU,UAAU,CAAC;;;;;;AAM5D,UAAO;;EAEV"}
|
package/dist/index.d.cts
CHANGED
|
@@ -36,16 +36,16 @@ type ResolvedConfigLike = {
|
|
|
36
36
|
* @example
|
|
37
37
|
* ```ts
|
|
38
38
|
* // vite.config.ts
|
|
39
|
-
* import
|
|
39
|
+
* import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
40
40
|
*
|
|
41
41
|
* export default defineConfig({
|
|
42
42
|
* plugins: [
|
|
43
|
-
*
|
|
43
|
+
* vitePluginVueTransitionRootValidator()
|
|
44
44
|
* ]
|
|
45
45
|
* });
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
|
-
declare function
|
|
48
|
+
declare function vitePluginVueTransitionRootValidator(): {
|
|
49
49
|
name: string;
|
|
50
50
|
apply: "serve";
|
|
51
51
|
/**
|
|
@@ -59,14 +59,14 @@ declare function vitePluginVueRootValidator(): {
|
|
|
59
59
|
configureServer(server: DevServerLike): void;
|
|
60
60
|
/**
|
|
61
61
|
* 解析虚拟模块
|
|
62
|
-
* 处理 'virtual:vue-root-validator' 模块的导入
|
|
62
|
+
* 处理 'virtual:vue-transition-root-validator' 模块的导入
|
|
63
63
|
*/
|
|
64
|
-
resolveId(id: string): "\0virtual:vue-root-validator" | null;
|
|
64
|
+
resolveId(id: string): "\0virtual:vue-transition-root-validator" | null;
|
|
65
65
|
/**
|
|
66
66
|
* 加载虚拟模块
|
|
67
67
|
* 返回虚拟模块的代码内容
|
|
68
68
|
*/
|
|
69
69
|
load(id: string): string | null;
|
|
70
70
|
};
|
|
71
|
-
export =
|
|
72
|
-
//# sourceMappingURL=index-
|
|
71
|
+
export = vitePluginVueTransitionRootValidator;
|
|
72
|
+
//# sourceMappingURL=index-BnGy14AF.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -36,16 +36,16 @@ type ResolvedConfigLike = {
|
|
|
36
36
|
* @example
|
|
37
37
|
* ```ts
|
|
38
38
|
* // vite.config.ts
|
|
39
|
-
* import
|
|
39
|
+
* import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
40
40
|
*
|
|
41
41
|
* export default defineConfig({
|
|
42
42
|
* plugins: [
|
|
43
|
-
*
|
|
43
|
+
* vitePluginVueTransitionRootValidator()
|
|
44
44
|
* ]
|
|
45
45
|
* });
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
|
-
declare function
|
|
48
|
+
declare function vitePluginVueTransitionRootValidator(): {
|
|
49
49
|
name: string;
|
|
50
50
|
apply: "serve";
|
|
51
51
|
/**
|
|
@@ -59,9 +59,9 @@ declare function vitePluginVueRootValidator(): {
|
|
|
59
59
|
configureServer(server: DevServerLike): void;
|
|
60
60
|
/**
|
|
61
61
|
* 解析虚拟模块
|
|
62
|
-
* 处理 'virtual:vue-root-validator' 模块的导入
|
|
62
|
+
* 处理 'virtual:vue-transition-root-validator' 模块的导入
|
|
63
63
|
*/
|
|
64
|
-
resolveId(id: string): "\0virtual:vue-root-validator" | null;
|
|
64
|
+
resolveId(id: string): "\0virtual:vue-transition-root-validator" | null;
|
|
65
65
|
/**
|
|
66
66
|
* 加载虚拟模块
|
|
67
67
|
* 返回虚拟模块的代码内容
|
|
@@ -69,5 +69,5 @@ declare function vitePluginVueRootValidator(): {
|
|
|
69
69
|
load(id: string): string | null;
|
|
70
70
|
};
|
|
71
71
|
//#endregion
|
|
72
|
-
export {
|
|
73
|
-
//# sourceMappingURL=index-
|
|
72
|
+
export { vitePluginVueTransitionRootValidator as default };
|
|
73
|
+
//# sourceMappingURL=index-BGyd2pvz.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -9,14 +9,14 @@ const translations = {
|
|
|
9
9
|
如何在项目中启用此插件:
|
|
10
10
|
|
|
11
11
|
1. 在 vite.config.ts 中添加插件:
|
|
12
|
-
import
|
|
12
|
+
import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
13
13
|
|
|
14
14
|
plugins: [
|
|
15
|
-
|
|
15
|
+
vitePluginVueTransitionRootValidator()
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
2. 在 src/main.ts 中初始化:
|
|
19
|
-
import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
19
|
+
import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
20
20
|
|
|
21
21
|
const app = createApp(App);
|
|
22
22
|
setupVueRootValidator(app, { lang: 'zh' });
|
|
@@ -29,14 +29,14 @@ const translations = {
|
|
|
29
29
|
How to enable this plugin in your project:
|
|
30
30
|
|
|
31
31
|
1. Add plugin in vite.config.ts:
|
|
32
|
-
import
|
|
32
|
+
import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
33
33
|
|
|
34
34
|
plugins: [
|
|
35
|
-
|
|
35
|
+
vitePluginVueTransitionRootValidator()
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
2. Initialize in src/main.ts:
|
|
39
|
-
import { setupVueRootValidator } from 'virtual:vue-root-validator';
|
|
39
|
+
import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';
|
|
40
40
|
|
|
41
41
|
const app = createApp(App);
|
|
42
42
|
setupVueRootValidator(app, { lang: 'en' });
|
|
@@ -78,16 +78,16 @@ function sendErrorOverlay(args) {
|
|
|
78
78
|
* @example
|
|
79
79
|
* ```ts
|
|
80
80
|
* // vite.config.ts
|
|
81
|
-
* import
|
|
81
|
+
* import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';
|
|
82
82
|
*
|
|
83
83
|
* export default defineConfig({
|
|
84
84
|
* plugins: [
|
|
85
|
-
*
|
|
85
|
+
* vitePluginVueTransitionRootValidator()
|
|
86
86
|
* ]
|
|
87
87
|
* });
|
|
88
88
|
* ```
|
|
89
89
|
*/
|
|
90
|
-
function
|
|
90
|
+
function vitePluginVueTransitionRootValidator() {
|
|
91
91
|
let resolved;
|
|
92
92
|
return {
|
|
93
93
|
name: "vite-plugin-vue-transition-root-validator",
|
|
@@ -117,16 +117,16 @@ function vitePluginVueRootValidator() {
|
|
|
117
117
|
});
|
|
118
118
|
},
|
|
119
119
|
resolveId(id) {
|
|
120
|
-
if (id === "virtual:vue-root-validator") return "\0virtual:vue-root-validator";
|
|
120
|
+
if (id === "virtual:vue-transition-root-validator") return "\0virtual:vue-transition-root-validator";
|
|
121
121
|
return null;
|
|
122
122
|
},
|
|
123
123
|
load(id) {
|
|
124
|
-
if (id === "\0virtual:vue-root-validator") {
|
|
124
|
+
if (id === "\0virtual:vue-transition-root-validator") {
|
|
125
125
|
const clientEntryTs = fileURLToPath(new URL("./client.ts", import.meta.url));
|
|
126
126
|
const clientEntryJs = fileURLToPath(new URL("./client.js", import.meta.url));
|
|
127
127
|
const clientUrl = `/@fs/${(existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs).replace(/\\/g, "/")}`;
|
|
128
128
|
return `
|
|
129
|
-
// 虚拟模块:vue-root-validator
|
|
129
|
+
// 虚拟模块:vue-transition-root-validator
|
|
130
130
|
// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成
|
|
131
131
|
|
|
132
132
|
import { setupVueRootValidator } from ${JSON.stringify(clientUrl)};
|
|
@@ -141,5 +141,5 @@ export { setupVueRootValidator };
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
//#endregion
|
|
144
|
-
export {
|
|
144
|
+
export { vitePluginVueTransitionRootValidator as default };
|
|
145
145
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/i18n.ts","../src/index.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport type { Lang } from './types.ts';\nimport { getMessageHeader } from './i18n.ts';\n\n/**\n * Vite DevServer 类型(精简版)\n */\ntype DevServerLike = {\n ws: {\n send: (payload: { type: 'error'; err: { message: string; stack?: string } }) => void;\n on: (event: string, listener: (payload: any, client: any) => void) => void;\n };\n config: {\n logger: {\n warn: (msg: string) => void;\n info: (msg: string) => void;\n };\n };\n};\n\ntype DevClientLike = {\n send?: {\n (payload: { type: 'error'; err: { message: string; stack?: string } }): void;\n (event: string, payload?: any): void;\n };\n};\n\n/**\n * Vite ResolvedConfig 类型(精简版)\n */\ntype ResolvedConfigLike = {\n command: 'serve' | 'build' | string;\n};\n\n/**\n * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)\n */\nfunction sendErrorOverlay(args: { server: DevServerLike; message: string; lang: Lang; client?: DevClientLike }) {\n const { server, message, lang, client } = args;\n const payload = {\n type: 'error',\n err: {\n message: getMessageHeader(lang),\n stack: message\n }\n } as const;\n\n if (client?.send) {\n client.send(payload);\n return;\n }\n\n server.ws.send(payload);\n}\n\n/**\n * 客户端上报的消息载荷\n */\ntype ClientReportPayload = {\n message: string;\n /** 用于 ACK 去重/确认(客户端可不传,服务端会回退使用 message 截断值) */\n key?: string;\n /** 客户端运行时语言(推荐从 main.ts 传入并上报),用于决定 overlay header 语言 */\n lang?: Lang;\n};\n\n/**\n * Vite 插件:Vue Root Validator\n *\n * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题\n *\n * @returns Vite 插件对象\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';\n *\n * export default defineConfig({\n * plugins: [\n * vitePluginVueRootValidator()\n * ]\n * });\n * ```\n */\nexport default function vitePluginVueRootValidator() {\n let resolved: ResolvedConfigLike;\n\n return {\n name: 'vite-plugin-vue-transition-root-validator',\n apply: 'serve' as const, // 仅在开发环境应用\n\n /**\n * 保存解析后的配置\n */\n configResolved(config: ResolvedConfigLike) {\n resolved = config;\n },\n\n /**\n * 配置开发服务器\n * 监听客户端上报的警告消息,并通过 error overlay 显示\n */\n configureServer(server: DevServerLike) {\n if (resolved.command !== 'serve') return;\n\n // 用于去重,避免同一客户端重复发送相同消息\n const lastByClient = new WeakMap<object, string>();\n\n // 监听客户端上报的警告\n server.ws.on('vite-plugin-vue-transition-root-validator:vue-warn', (payload: ClientReportPayload, client: object) => {\n if (!payload?.message) return;\n\n // 去重处理\n const key = payload.key ?? payload.message.slice(0, 800);\n const prev = lastByClient.get(client as unknown as object);\n if (prev === key) {\n // 仍然回 ACK,避免客户端重试积压\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n return;\n }\n lastByClient.set(client as unknown as object, key);\n\n const effectiveLang: Lang = payload.lang ?? 'en';\n\n // 发送错误覆盖层\n sendErrorOverlay({\n server,\n message: payload.message,\n lang: effectiveLang,\n client: client as any as DevClientLike\n });\n\n // 回 ACK,通知客户端该消息已被处理(用于清理重试队列)\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n });\n },\n\n /**\n * 解析虚拟模块\n * 处理 'virtual:vue-root-validator' 模块的导入\n */\n resolveId(id: string) {\n if (id === 'virtual:vue-root-validator') {\n // 返回一个虚拟模块 ID,加上 \\0 前缀表示这是一个虚拟模块\n return '\\0virtual:vue-root-validator';\n }\n return null;\n },\n\n /**\n * 加载虚拟模块\n * 返回虚拟模块的代码内容\n */\n load(id: string) {\n if (id === '\\0virtual:vue-root-validator') {\n const clientEntryTs = fileURLToPath(new URL('./client.ts', import.meta.url));\n const clientEntryJs = fileURLToPath(new URL('./client.js', import.meta.url));\n const clientEntry = existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs;\n const clientUrl = `/@fs/${clientEntry.replace(/\\\\/g, '/')}`;\n\n // 返回虚拟模块代码:重新导出 client.ts 的函数\n return `\n// 虚拟模块:vue-root-validator\n// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成\n\nimport { setupVueRootValidator } from ${JSON.stringify(clientUrl)};\n\n// 重新导出函数\nexport { setupVueRootValidator };\n`;\n }\n return null;\n }\n };\n}\n\n\n"],"mappings":";;;;AASA,MAAM,eAAe;CACnB,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACD,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACF;AAiED,SAAgB,iBAAiB,MAAoB;AACnD,QAAO,aAAa,MAAM;;;;;;;;AC9E5B,SAAS,iBAAiB,MAAsF;CAC9G,MAAM,EAAE,QAAQ,SAAS,MAAM,WAAW;CAC1C,MAAM,UAAU;EACd,MAAM;EACN,KAAK;GACH,SAAS,iBAAiB,KAAK;GAC/B,OAAO;GACR;EACF;AAED,KAAI,QAAQ,MAAM;AAChB,SAAO,KAAK,QAAQ;AACpB;;AAGF,QAAO,GAAG,KAAK,QAAQ;;;;;;;;;;;;;;;;;;;;;AAiCzB,SAAwB,6BAA6B;CACnD,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAKP,eAAe,QAA4B;AACzC,cAAW;;EAOb,gBAAgB,QAAuB;AACrC,OAAI,SAAS,YAAY,QAAS;GAGlC,MAAM,+BAAe,IAAI,SAAyB;AAGlD,UAAO,GAAG,GAAG,uDAAuD,SAA8B,WAAmB;AACnH,QAAI,CAAC,SAAS,QAAS;IAGvB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAExD,QADa,aAAa,IAAI,OAA4B,KAC7C,KAAK;AAEhB,KAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;AAClG;;AAEF,iBAAa,IAAI,QAA6B,IAAI;IAElD,MAAM,gBAAsB,QAAQ,QAAQ;AAG5C,qBAAiB;KACf;KACA,SAAS,QAAQ;KACjB,MAAM;KACE;KACT,CAAC;AAGF,IAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;KAClG;;EAOJ,UAAU,IAAY;AACpB,OAAI,OAAO,6BAET,QAAO;AAET,UAAO;;EAOT,KAAK,IAAY;AACf,OAAI,OAAO,gCAAgC;IACzC,MAAM,gBAAgB,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,IAAI,CAAC;IAC5E,MAAM,gBAAgB,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,IAAI,CAAC;IAE5E,MAAM,YAAY,SADE,WAAW,cAAc,GAAG,gBAAgB,eAC1B,QAAQ,OAAO,IAAI;AAGzD,WAAO;;;;wCAIyB,KAAK,UAAU,UAAU,CAAC;;;;;;AAM5D,UAAO;;EAEV"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/i18n.ts","../src/index.ts"],"sourcesContent":["import type { Lang } from './types.ts';\n\nexport type TransitionRootMessageContext = {\n file?: string;\n url?: string;\n routeKey?: string;\n component?: string;\n};\n\nconst translations = {\n zh: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误',\n setupInstructions: `\n如何在项目中启用此插件:\n\n1. 在 vite.config.ts 中添加插件:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. 在 src/main.ts 中初始化:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'zh' });\n app.mount('#app');\n`\n },\n en: {\n messageHeader: '[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error',\n setupInstructions: `\nHow to enable this plugin in your project:\n\n1. Add plugin in vite.config.ts:\n import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n\n plugins: [\n vitePluginVueTransitionRootValidator()\n ]\n\n2. Initialize in src/main.ts:\n import { setupVueRootValidator } from 'virtual:vue-transition-root-validator';\n\n const app = createApp(App);\n setupVueRootValidator(app, { lang: 'en' });\n app.mount('#app');\n`\n }\n};\n\nexport function formatTransitionRootMessage(lang: Lang, ctx: TransitionRootMessageContext): string {\n if (lang === 'zh') {\n const title = 'Vue <Transition> 要求插槽内容具有单一的“元素根节点”。';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\n文件: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`上下文: ${meta.join(' ')}`);\n\n lines.push(\n '\\n如何修复:\\n' +\n `- 在${ctx.file ? `文件 ${ctx.file}` : '该组件'}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\\n` +\n '- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。\\n' +\n '- 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。'\n );\n\n lines.push(\n '\\n为什么会这样:\\n' +\n 'Vue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;' +\n '当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。'\n );\n\n lines.push('\\n相关文档:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n }\n\n // English\n const title = 'Vue <Transition> requires a single element root node in its slot.';\n const lines: string[] = [];\n lines.push(title);\n\n if (ctx.file) lines.push(`\\nFile: ${ctx.file}`);\n if (ctx.url) lines.push(`URL: ${ctx.url}`);\n\n const meta: string[] = [];\n if (ctx.component) meta.push(`component=${ctx.component}`);\n if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);\n if (meta.length) lines.push(`Context: ${meta.join(' ')}`);\n\n lines.push(\n '\\nHow to fix:\\n' +\n `- In the <template> of ${ctx.file ? `file ${ctx.file}` : 'this component'}, wrap everything with a single container element (e.g. <div> / <main>), so the final render has exactly one root *element*.\\n` +\n '- The root cannot be a Fragment (multiple siblings), plain text, or a comment.\\n' +\n '- If you use v-if / v-else at the root, ensure each branch renders exactly one root element.'\n );\n\n lines.push(\n '\\nWhy this happens:\\n' +\n 'Vue <Transition> needs to apply transition classes to a real DOM element. ' +\n 'If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.'\n );\n\n lines.push('\\nDocs:\\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component');\n\n return lines.join('\\n');\n}\n\nexport function getMessageHeader(lang: Lang): string {\n return translations[lang].messageHeader;\n}\n\nexport function getSetupInstructions(lang: Lang): string {\n return translations[lang].setupInstructions;\n}\n","import { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport type { Lang } from './types.ts';\nimport { getMessageHeader } from './i18n.ts';\n\n/**\n * Vite DevServer 类型(精简版)\n */\ntype DevServerLike = {\n ws: {\n send: (payload: { type: 'error'; err: { message: string; stack?: string } }) => void;\n on: (event: string, listener: (payload: any, client: any) => void) => void;\n };\n config: {\n logger: {\n warn: (msg: string) => void;\n info: (msg: string) => void;\n };\n };\n};\n\ntype DevClientLike = {\n send?: {\n (payload: { type: 'error'; err: { message: string; stack?: string } }): void;\n (event: string, payload?: any): void;\n };\n};\n\n/**\n * Vite ResolvedConfig 类型(精简版)\n */\ntype ResolvedConfigLike = {\n command: 'serve' | 'build' | string;\n};\n\n/**\n * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)\n */\nfunction sendErrorOverlay(args: { server: DevServerLike; message: string; lang: Lang; client?: DevClientLike }) {\n const { server, message, lang, client } = args;\n const payload = {\n type: 'error',\n err: {\n message: getMessageHeader(lang),\n stack: message\n }\n } as const;\n\n if (client?.send) {\n client.send(payload);\n return;\n }\n\n server.ws.send(payload);\n}\n\n/**\n * 客户端上报的消息载荷\n */\ntype ClientReportPayload = {\n message: string;\n /** 用于 ACK 去重/确认(客户端可不传,服务端会回退使用 message 截断值) */\n key?: string;\n /** 客户端运行时语言(推荐从 main.ts 传入并上报),用于决定 overlay header 语言 */\n lang?: Lang;\n};\n\n/**\n * Vite 插件:Vue Root Validator\n *\n * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题\n *\n * @returns Vite 插件对象\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vitePluginVueTransitionRootValidator from 'vite-plugin-vue-transition-root-validator';\n *\n * export default defineConfig({\n * plugins: [\n * vitePluginVueTransitionRootValidator()\n * ]\n * });\n * ```\n */\nexport default function vitePluginVueTransitionRootValidator() {\n let resolved: ResolvedConfigLike;\n\n return {\n name: 'vite-plugin-vue-transition-root-validator',\n apply: 'serve' as const, // 仅在开发环境应用\n\n /**\n * 保存解析后的配置\n */\n configResolved(config: ResolvedConfigLike) {\n resolved = config;\n },\n\n /**\n * 配置开发服务器\n * 监听客户端上报的警告消息,并通过 error overlay 显示\n */\n configureServer(server: DevServerLike) {\n if (resolved.command !== 'serve') return;\n\n // 用于去重,避免同一客户端重复发送相同消息\n const lastByClient = new WeakMap<object, string>();\n\n // 监听客户端上报的警告\n server.ws.on('vite-plugin-vue-transition-root-validator:vue-warn', (payload: ClientReportPayload, client: object) => {\n if (!payload?.message) return;\n\n // 去重处理\n const key = payload.key ?? payload.message.slice(0, 800);\n const prev = lastByClient.get(client as unknown as object);\n if (prev === key) {\n // 仍然回 ACK,避免客户端重试积压\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n return;\n }\n lastByClient.set(client as unknown as object, key);\n\n const effectiveLang: Lang = payload.lang ?? 'en';\n\n // 发送错误覆盖层\n sendErrorOverlay({\n server,\n message: payload.message,\n lang: effectiveLang,\n client: client as any as DevClientLike\n });\n\n // 回 ACK,通知客户端该消息已被处理(用于清理重试队列)\n (client as any as DevClientLike)?.send?.('vite-plugin-vue-transition-root-validator:ack', { key });\n });\n },\n\n /**\n * 解析虚拟模块\n * 处理 'virtual:vue-transition-root-validator' 模块的导入\n */\n resolveId(id: string) {\n if (id === 'virtual:vue-transition-root-validator') {\n // 返回一个虚拟模块 ID,加上 \\0 前缀表示这是一个虚拟模块\n return '\\0virtual:vue-transition-root-validator';\n }\n return null;\n },\n\n /**\n * 加载虚拟模块\n * 返回虚拟模块的代码内容\n */\n load(id: string) {\n if (id === '\\0virtual:vue-transition-root-validator') {\n const clientEntryTs = fileURLToPath(new URL('./client.ts', import.meta.url));\n const clientEntryJs = fileURLToPath(new URL('./client.js', import.meta.url));\n const clientEntry = existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs;\n const clientUrl = `/@fs/${clientEntry.replace(/\\\\/g, '/')}`;\n\n // 返回虚拟模块代码:重新导出 client.ts 的函数\n return `\n// 虚拟模块:vue-transition-root-validator\n// 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成\n\nimport { setupVueRootValidator } from ${JSON.stringify(clientUrl)};\n\n// 重新导出函数\nexport { setupVueRootValidator };\n`;\n }\n return null;\n }\n };\n}\n\n\n"],"mappings":";;;;AASA,MAAM,eAAe;CACnB,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACD,IAAI;EACF,eAAe;EACf,mBAAmB;;;;;;;;;;;;;;;;;EAiBpB;CACF;AAiED,SAAgB,iBAAiB,MAAoB;AACnD,QAAO,aAAa,MAAM;;;;;;;;AC9E5B,SAAS,iBAAiB,MAAsF;CAC9G,MAAM,EAAE,QAAQ,SAAS,MAAM,WAAW;CAC1C,MAAM,UAAU;EACd,MAAM;EACN,KAAK;GACH,SAAS,iBAAiB,KAAK;GAC/B,OAAO;GACR;EACF;AAED,KAAI,QAAQ,MAAM;AAChB,SAAO,KAAK,QAAQ;AACpB;;AAGF,QAAO,GAAG,KAAK,QAAQ;;;;;;;;;;;;;;;;;;;;;AAiCzB,SAAwB,uCAAuC;CAC7D,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAKP,eAAe,QAA4B;AACzC,cAAW;;EAOb,gBAAgB,QAAuB;AACrC,OAAI,SAAS,YAAY,QAAS;GAGlC,MAAM,+BAAe,IAAI,SAAyB;AAGlD,UAAO,GAAG,GAAG,uDAAuD,SAA8B,WAAmB;AACnH,QAAI,CAAC,SAAS,QAAS;IAGvB,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAExD,QADa,aAAa,IAAI,OAA4B,KAC7C,KAAK;AAEhB,KAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;AAClG;;AAEF,iBAAa,IAAI,QAA6B,IAAI;IAElD,MAAM,gBAAsB,QAAQ,QAAQ;AAG5C,qBAAiB;KACf;KACA,SAAS,QAAQ;KACjB,MAAM;KACE;KACT,CAAC;AAGF,IAAC,QAAiC,OAAO,iDAAiD,EAAE,KAAK,CAAC;KAClG;;EAOJ,UAAU,IAAY;AACpB,OAAI,OAAO,wCAET,QAAO;AAET,UAAO;;EAOT,KAAK,IAAY;AACf,OAAI,OAAO,2CAA2C;IACpD,MAAM,gBAAgB,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,IAAI,CAAC;IAC5E,MAAM,gBAAgB,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,IAAI,CAAC;IAE5E,MAAM,YAAY,SADE,WAAW,cAAc,GAAG,gBAAgB,eAC1B,QAAQ,OAAO,IAAI;AAGzD,WAAO;;;;wCAIyB,KAAK,UAAU,UAAU,CAAC;;;;;;AAM5D,UAAO;;EAEV"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-vue-transition-root-validator",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Capture Vue <Transition> runtime warnings and show actionable overlay in Vite dev.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"azir",
|
|
8
|
+
"vite",
|
|
9
|
+
"vue",
|
|
10
|
+
"vue-router",
|
|
11
|
+
"router",
|
|
12
|
+
"transition",
|
|
13
|
+
"root",
|
|
14
|
+
"validator"
|
|
15
|
+
],
|
|
6
16
|
"license": "MIT",
|
|
7
17
|
"files": [
|
|
8
18
|
"dist"
|
|
@@ -20,14 +30,18 @@
|
|
|
20
30
|
"types": "./dist/index.d.cts",
|
|
21
31
|
"default": "./dist/index.cjs"
|
|
22
32
|
}
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./dist/client.d.ts"
|
|
23
36
|
}
|
|
24
37
|
},
|
|
25
38
|
"scripts": {
|
|
26
|
-
"build": "tsdown src/index.ts --format esm,cjs --dts --sourcemap --clean && mv dist/index-*.d.ts dist/index.d.ts && mv dist/index-*.d.cts dist/index.d.cts && tsdown src/client.ts --format esm --dts --sourcemap --no-clean && publint",
|
|
39
|
+
"build": "tsdown src/index.ts --format esm,cjs --dts --sourcemap --clean && mv dist/index-*.d.ts dist/index.d.ts && mv dist/index-*.d.cts dist/index.d.cts && tsdown src/client.ts --format esm --dts --sourcemap --no-clean && mv dist/client-*.d.ts dist/client.d.ts && echo \"\" >> dist/client.d.ts && cat src/env.d.ts >> dist/client.d.ts && publint",
|
|
27
40
|
"test": "vitest run",
|
|
28
41
|
"typecheck": "tsc -p tsconfig.json",
|
|
29
|
-
"verify": "
|
|
42
|
+
"verify": "pnpm run build && attw --pack ."
|
|
30
43
|
},
|
|
44
|
+
"packageManager": "pnpm@10.14.0",
|
|
31
45
|
"peerDependencies": {
|
|
32
46
|
"vite": ">=4.0.0",
|
|
33
47
|
"vue": ">=3.0.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-BIaiUtvr.d.cts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;AAQkB;AAuBK,KAvBlB,aAAA,GA8EmB;EAA0B,EAAA,EAAA;IAUvB,IAAA,EAAA,CAAA,OAAA,EAAA;MAQC,IAAA,EAAA,OAAA;MAAa,GAAA,EAAA;;;;;;;;;;;;;;;;;KAzEpC,kBAAA;;;;;;;;;;;;;;;;;;;;;;iBAuDmB,0BAAA,CAAA;;;;;;yBAUG;;;;;0BAQC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-BPjVwmbE.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;AAQkB;AAuBK,KAvBlB,aAAA,GA8EmB;EAA0B,EAAA,EAAA;IAUvB,IAAA,EAAA,CAAA,OAAA,EAAA;MAQC,IAAA,EAAA,OAAA;MAAa,GAAA,EAAA;;;;;;;;;;;;;;;;;KAzEpC,kBAAA;;;;;;;;;;;;;;;;;;;;;;iBAuDmB,0BAAA,CAAA;;;;;;yBAUG;;;;;0BAQC"}
|