vite-plugin-vue-transition-root-validator 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 青菜白玉汤
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # vite-plugin-vue-transition-root-validator
2
+ 在 Vite dev 环境捕获 Vue `<Transition>` 运行时 warn,并用 overlay 给出可操作的修复建议。
3
+
4
+ ## 安装
5
+
6
+ ```bash
7
+ npm i -D vite-plugin-vue-transition-root-validator
8
+ ```
9
+
10
+ ## 使用
11
+
12
+ 在 `vite.config.ts` 中:
13
+
14
+ ```ts
15
+ import { defineConfig } from 'vite'
16
+ import vue from '@vitejs/plugin-vue'
17
+ import vueRootValidator from 'vite-plugin-vue-transition-root-validator'
18
+
19
+ export default defineConfig({
20
+ plugins: [
21
+ vue(),
22
+ // 需要启用该 Vite 插件(用于注入虚拟模块 + 监听客户端上报)
23
+ // 本插件不提供任何 vite.config 参数。
24
+ vueRootValidator()
25
+ ]
26
+ })
27
+ ```
28
+
29
+ 在 `main.ts` 中:
30
+
31
+ ```ts
32
+ import { createApp } from 'vue';
33
+ import App from './App.vue';
34
+ import { setupVueRootValidator } from 'virtual:vue-root-validator';
35
+
36
+ const app = createApp(App);
37
+
38
+ // 在 mount 前初始化(推荐放在所有 setup 之后)
39
+ setupVueRootValidator(app, {
40
+ lang: 'zh' // ✅ 语言只需要在这里配置一次
41
+ });
42
+
43
+ app.mount('#app');
44
+ ```
45
+
46
+ > 说明:overlay 的标题(message header)与正文(stack)都会跟随此处的 `lang`。
47
+
48
+
49
+ ## 代码执行流程
50
+
51
+ ### 插件初始化阶段(构建时)
52
+
53
+ ```
54
+ vite.config.ts
55
+ └─> setupVitePlugins()
56
+ └─> vueRootValidator({ lang: 'zh' })
57
+ └─> [index.ts] vitePluginVueRootValidator()
58
+ ├─> configResolved() - 保存配置
59
+ ├─> configureServer() - 监听 WebSocket 消息
60
+ ├─> resolveId() - 注册虚拟模块 'virtual:vue-root-validator'
61
+ └─> load() - 返回虚拟模块代码(导出 client.ts 的函数)
62
+ ```
63
+
64
+ ### 应用启动阶段(运行时)
65
+
66
+ ```
67
+ src/main.ts
68
+ └─> setupApp()
69
+ ├─> createApp(App)
70
+ ├─> setupStore(app)
71
+ ├─> setupRouter(app)
72
+ ├─> setupI18n(app)
73
+
74
+ ├─> 【新增】setupVueRootValidator(app) ⭐
75
+ │ └─> [client.ts] setupVueRootValidator()
76
+ │ ├─> 注册 app.config.warnHandler
77
+ │ ├─> 拦截所有 Vue 警告
78
+ │ └─> 筛选 Transition 多根节点警告
79
+
80
+ └─> app.mount('#app')
81
+ ```
82
+
83
+ ### 错误检测阶段(运行时)
84
+
85
+ ```
86
+ 浏览器运行
87
+ └─> Vue 检测到 <Transition> 多根问题
88
+ └─> Vue 内部调用 app.config.warnHandler()
89
+ └─> [client.ts] 自定义 warnHandler
90
+ ├─> 检查是否是 Transition 警告 ✓
91
+ ├─> 从组件追踪栈提取信息
92
+ │ ├─> extractRouteKey() - 提取路由 key
93
+ │ ├─> extractComponentName() - 提取组件名
94
+ │ └─> guessViewFileFromRouteKey() - 推测文件路径
95
+
96
+ ├─> formatTransitionRootMessage() - 格式化错误消息
97
+ │ └─> [i18n.ts] 生成中文错误提示
98
+
99
+ └─> send() - 通过 HMR WebSocket 发送到服务器
100
+ ```
101
+
102
+ ### 错误展示阶段(服务端)
103
+
104
+ ```
105
+ Vite 开发服务器
106
+ └─> [index.ts] configureServer()
107
+ └─> server.ws.on('vite-plugin-vue-transition-root-validator:vue-warn')
108
+ ├─> 接收客户端上报的错误消息
109
+ ├─> 去重处理(避免重复显示)
110
+ ├─> sendErrorOverlay() - 发送错误覆盖层
111
+ │ └─> server.ws.send({ type: 'error', ... })
112
+ │ └─> 浏览器显示 Vite Error Overlay 🔴
113
+
114
+ └─> 首次错误时显示初始化说明(控制台)
115
+ ```
@@ -0,0 +1,42 @@
1
+ import { App } from "vue";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ type Lang = 'en' | 'zh';
6
+ //#endregion
7
+ //#region src/client.d.ts
8
+ /**
9
+ * 客户端配置选项
10
+ */
11
+ type SetupOptions = {
12
+ /** 界面语言,默认 'en' */
13
+ lang?: Lang;
14
+ /** 是否在检测到错误后自动禁用检测(避免重复报错),默认 false */
15
+ disableAfterFirstError?: boolean;
16
+ };
17
+ /**
18
+ * 设置 Vue Root Validator
19
+ *
20
+ * 在 Vue 应用上注册 warnHandler 来捕获 Transition 多根节点警告
21
+ *
22
+ * @param app - Vue 应用实例
23
+ * @param options - 配置选项
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { createApp } from 'vue';
28
+ * import { setupVueRootValidator } from 'virtual:vue-root-validator';
29
+ * import App from './App.vue';
30
+ *
31
+ * const app = createApp(App);
32
+ *
33
+ * // 在挂载前设置验证器
34
+ * setupVueRootValidator(app, { lang: 'zh' });
35
+ *
36
+ * app.mount('#app');
37
+ * ```
38
+ */
39
+ declare function setupVueRootValidator(app: App, options?: SetupOptions): void;
40
+ //#endregion
41
+ export { setupVueRootValidator };
42
+ //# sourceMappingURL=client-C1tyP3h4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-C1tyP3h4.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"}
package/dist/client.js ADDED
@@ -0,0 +1,224 @@
1
+ //#region src/i18n.ts
2
+ function formatTransitionRootMessage(lang, ctx) {
3
+ if (lang === "zh") {
4
+ const title$1 = "Vue <Transition> 要求插槽内容具有单一的“元素根节点”。";
5
+ const lines$1 = [];
6
+ lines$1.push(title$1);
7
+ if (ctx.file) lines$1.push(`\n文件: ${ctx.file}`);
8
+ if (ctx.url) lines$1.push(`URL: ${ctx.url}`);
9
+ const meta$1 = [];
10
+ if (ctx.component) meta$1.push(`component=${ctx.component}`);
11
+ if (ctx.routeKey) meta$1.push(`key=${ctx.routeKey}`);
12
+ if (meta$1.length) lines$1.push(`上下文: ${meta$1.join(" ")}`);
13
+ lines$1.push(`
14
+ 如何修复:
15
+ - 在${ctx.file ? `文件 ${ctx.file}` : "该组件"}的 <template> 最外层添加一个容器标签(如 <div> / <main>),把所有内容包起来,确保最终只渲染出一个根“标签元素”。\n- 根节点不能是多个并列元素(Fragment/多根),也不能是纯文本或注释。
16
+ - 如果根部使用了 v-if / v-else,确保每个分支都只渲染一个根标签元素。`);
17
+ lines$1.push("\n为什么会这样:\nVue 的 <Transition> 需要把过渡 class 应用在一个真实的 DOM 元素上;当插槽内容渲染出的根节点不是“单一元素”(例如 Fragment、多根、纯文本或注释)时,就无法执行进入/离开过渡。");
18
+ lines$1.push("\n相关文档:\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component");
19
+ return lines$1.join("\n");
20
+ }
21
+ const title = "Vue <Transition> requires a single element root node in its slot.";
22
+ const lines = [];
23
+ lines.push(title);
24
+ if (ctx.file) lines.push(`\nFile: ${ctx.file}`);
25
+ if (ctx.url) lines.push(`URL: ${ctx.url}`);
26
+ const meta = [];
27
+ if (ctx.component) meta.push(`component=${ctx.component}`);
28
+ if (ctx.routeKey) meta.push(`key=${ctx.routeKey}`);
29
+ if (meta.length) lines.push(`Context: ${meta.join(" ")}`);
30
+ lines.push(`
31
+ How to fix:
32
+ - 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- The root cannot be a Fragment (multiple siblings), plain text, or a comment.
33
+ - If you use v-if / v-else at the root, ensure each branch renders exactly one root element.`);
34
+ lines.push("\nWhy this happens:\nVue <Transition> needs to apply transition classes to a real DOM element. If the slot content renders a non-element root (Fragment/multiple roots/text/comment), Vue cannot animate it.");
35
+ lines.push("\nDocs:\nhttps://cn.vuejs.org/guide/built-ins/transition#the-transition-component");
36
+ return lines.join("\n");
37
+ }
38
+
39
+ //#endregion
40
+ //#region src/client.ts
41
+ /**
42
+ * Vue Transition 警告关键字
43
+ * 用于识别 Vue 运行时发出的 Transition 多根节点警告
44
+ */
45
+ const VUE_TRANSITION_WARN = "Component inside <Transition> renders non-element root node that cannot be animated.";
46
+ /**
47
+ * 从 Vue 警告信息中提取组件名称
48
+ * 示例: "at <Index onVnodeUnmounted=... key="/test" ... >"
49
+ */
50
+ function extractComponentName(text) {
51
+ return text.match(/at <([^\s>]+)/)?.[1];
52
+ }
53
+ /**
54
+ * 从 Vue 警告信息中提取路由 key
55
+ * 示例: key="/test"
56
+ */
57
+ function extractRouteKey(text) {
58
+ return text.match(/key=\"([^\"]+)\"/)?.[1];
59
+ }
60
+ /**
61
+ * 获取组件实例可能对应的文件路径
62
+ */
63
+ function guessViewFileFromInstance(instance) {
64
+ if (!instance) return void 0;
65
+ return instance.$options["__file"];
66
+ }
67
+ /**
68
+ * 获取组件实例可能对应的文件路径
69
+ */
70
+ function getViewUrlFromInstance(instance) {
71
+ if (!instance) return void 0;
72
+ return instance.$el?.baseURI;
73
+ }
74
+ /**
75
+ * 检查插件是否已经安装
76
+ */
77
+ function alreadyInstalled() {
78
+ return Boolean(globalThis["__VITE_PLUGIN_VUE_ROOT_VALIDATOR_INSTALLED__"]);
79
+ }
80
+ /**
81
+ * 标记插件已安装
82
+ */
83
+ function markInstalled() {
84
+ globalThis["__VITE_PLUGIN_VUE_ROOT_VALIDATOR_INSTALLED__"] = true;
85
+ }
86
+ const pendingPayloads = /* @__PURE__ */ new Map();
87
+ let listenersBound = false;
88
+ let retryTimer = null;
89
+ let retryDelayMs = 200;
90
+ function getHot() {
91
+ return import.meta.hot;
92
+ }
93
+ function trySendNow(payload) {
94
+ const hot = getHot();
95
+ if (!hot?.send) return false;
96
+ try {
97
+ hot.send("vite-plugin-vue-transition-root-validator:vue-warn", payload);
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function scheduleRetry() {
104
+ if (retryTimer !== null) return;
105
+ retryTimer = globalThis.setTimeout(() => {
106
+ retryTimer = null;
107
+ flushPendingPayloads();
108
+ if (pendingPayloads.size) {
109
+ retryDelayMs = Math.min(retryDelayMs * 2, 2e3);
110
+ scheduleRetry();
111
+ }
112
+ }, retryDelayMs);
113
+ }
114
+ function flushPendingPayloads() {
115
+ if (!pendingPayloads.size) return;
116
+ for (const p of pendingPayloads.values()) trySendNow(p);
117
+ }
118
+ function bindHmrListenersOnce() {
119
+ if (listenersBound) return;
120
+ listenersBound = true;
121
+ const hot = getHot();
122
+ if (!hot?.on) return;
123
+ hot.on("vite:ws:connect", () => {
124
+ retryDelayMs = 200;
125
+ if (retryTimer !== null) {
126
+ clearTimeout(retryTimer);
127
+ retryTimer = null;
128
+ }
129
+ flushPendingPayloads();
130
+ });
131
+ hot.on("vite-plugin-vue-transition-root-validator:ack", (data) => {
132
+ const key = data?.key;
133
+ if (!key) return;
134
+ pendingPayloads.delete(key);
135
+ if (!pendingPayloads.size) {
136
+ retryDelayMs = 200;
137
+ if (retryTimer !== null) {
138
+ clearTimeout(retryTimer);
139
+ retryTimer = null;
140
+ }
141
+ }
142
+ });
143
+ }
144
+ function send(payload) {
145
+ if (!getHot()?.send) return;
146
+ bindHmrListenersOnce();
147
+ pendingPayloads.set(payload.key, payload);
148
+ trySendNow(payload);
149
+ scheduleRetry();
150
+ }
151
+ let originalWarnHandler = null;
152
+ function resendLog(msg, instance, trace) {
153
+ if (originalWarnHandler) originalWarnHandler(msg, instance, trace);
154
+ }
155
+ /**
156
+ * 设置 Vue Root Validator
157
+ *
158
+ * 在 Vue 应用上注册 warnHandler 来捕获 Transition 多根节点警告
159
+ *
160
+ * @param app - Vue 应用实例
161
+ * @param options - 配置选项
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * import { createApp } from 'vue';
166
+ * import { setupVueRootValidator } from 'virtual:vue-root-validator';
167
+ * import App from './App.vue';
168
+ *
169
+ * const app = createApp(App);
170
+ *
171
+ * // 在挂载前设置验证器
172
+ * setupVueRootValidator(app, { lang: 'zh' });
173
+ *
174
+ * app.mount('#app');
175
+ * ```
176
+ */
177
+ function setupVueRootValidator(app, options = {}) {
178
+ if (alreadyInstalled()) return;
179
+ markInstalled();
180
+ const lang = options.lang ?? "en";
181
+ const disableAfterFirstError = options.disableAfterFirstError ?? false;
182
+ bindHmrListenersOnce();
183
+ originalWarnHandler = app.config.warnHandler;
184
+ let lastSentAt = 0;
185
+ let lastSentKey = "";
186
+ let errorSent = false;
187
+ /**
188
+ * 自定义 Vue 警告处理器
189
+ *
190
+ * @param msg - 警告消息
191
+ * @param instance - 组件实例
192
+ * @param trace - 组件追踪栈
193
+ */
194
+ app.config.warnHandler = (msg, instance, trace) => {
195
+ resendLog(msg, instance, trace);
196
+ if (disableAfterFirstError && errorSent) return;
197
+ if (!msg.includes(VUE_TRANSITION_WARN)) return;
198
+ const now = Date.now();
199
+ const key = (msg + trace).slice(0, 400);
200
+ if (now - lastSentAt > 500 || key !== lastSentKey) {
201
+ lastSentAt = now;
202
+ lastSentKey = key;
203
+ const routeKey = extractRouteKey(trace);
204
+ const component = extractComponentName(trace);
205
+ const file = guessViewFileFromInstance(instance);
206
+ const message = formatTransitionRootMessage(lang, {
207
+ url: getViewUrlFromInstance(instance),
208
+ file,
209
+ routeKey,
210
+ component
211
+ });
212
+ send({
213
+ key: message.slice(0, 800),
214
+ message,
215
+ lang
216
+ });
217
+ errorSent = true;
218
+ }
219
+ };
220
+ }
221
+
222
+ //#endregion
223
+ export { setupVueRootValidator };
224
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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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"}
@@ -0,0 +1 @@
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"}
@@ -0,0 +1 @@
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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,145 @@
1
+ let node_url = require("node:url");
2
+ let node_fs = require("node:fs");
3
+
4
+ //#region src/i18n.ts
5
+ const translations = {
6
+ zh: {
7
+ messageHeader: "[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误",
8
+ setupInstructions: `
9
+ 如何在项目中启用此插件:
10
+
11
+ 1. 在 vite.config.ts 中添加插件:
12
+ import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
13
+
14
+ plugins: [
15
+ vitePluginVueRootValidator()
16
+ ]
17
+
18
+ 2. 在 src/main.ts 中初始化:
19
+ import { setupVueRootValidator } from 'virtual:vue-root-validator';
20
+
21
+ const app = createApp(App);
22
+ setupVueRootValidator(app, { lang: 'zh' });
23
+ app.mount('#app');
24
+ `
25
+ },
26
+ en: {
27
+ messageHeader: "[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error",
28
+ setupInstructions: `
29
+ How to enable this plugin in your project:
30
+
31
+ 1. Add plugin in vite.config.ts:
32
+ import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
33
+
34
+ plugins: [
35
+ vitePluginVueRootValidator()
36
+ ]
37
+
38
+ 2. Initialize in src/main.ts:
39
+ import { setupVueRootValidator } from 'virtual:vue-root-validator';
40
+
41
+ const app = createApp(App);
42
+ setupVueRootValidator(app, { lang: 'en' });
43
+ app.mount('#app');
44
+ `
45
+ }
46
+ };
47
+ function getMessageHeader(lang) {
48
+ return translations[lang].messageHeader;
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/index.ts
53
+ /**
54
+ * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)
55
+ */
56
+ function sendErrorOverlay(args) {
57
+ const { server, message, lang, client } = args;
58
+ const payload = {
59
+ type: "error",
60
+ err: {
61
+ message: getMessageHeader(lang),
62
+ stack: message
63
+ }
64
+ };
65
+ if (client?.send) {
66
+ client.send(payload);
67
+ return;
68
+ }
69
+ server.ws.send(payload);
70
+ }
71
+ /**
72
+ * Vite 插件:Vue Root Validator
73
+ *
74
+ * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题
75
+ *
76
+ * @returns Vite 插件对象
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // vite.config.ts
81
+ * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
82
+ *
83
+ * export default defineConfig({
84
+ * plugins: [
85
+ * vitePluginVueRootValidator()
86
+ * ]
87
+ * });
88
+ * ```
89
+ */
90
+ function vitePluginVueRootValidator() {
91
+ let resolved;
92
+ return {
93
+ name: "vite-plugin-vue-transition-root-validator",
94
+ apply: "serve",
95
+ configResolved(config) {
96
+ resolved = config;
97
+ },
98
+ configureServer(server) {
99
+ if (resolved.command !== "serve") return;
100
+ const lastByClient = /* @__PURE__ */ new WeakMap();
101
+ server.ws.on("vite-plugin-vue-transition-root-validator:vue-warn", (payload, client) => {
102
+ if (!payload?.message) return;
103
+ const key = payload.key ?? payload.message.slice(0, 800);
104
+ if (lastByClient.get(client) === key) {
105
+ client?.send?.("vite-plugin-vue-transition-root-validator:ack", { key });
106
+ return;
107
+ }
108
+ lastByClient.set(client, key);
109
+ const effectiveLang = payload.lang ?? "en";
110
+ sendErrorOverlay({
111
+ server,
112
+ message: payload.message,
113
+ lang: effectiveLang,
114
+ client
115
+ });
116
+ client?.send?.("vite-plugin-vue-transition-root-validator:ack", { key });
117
+ });
118
+ },
119
+ resolveId(id) {
120
+ if (id === "virtual:vue-root-validator") return "\0virtual:vue-root-validator";
121
+ return null;
122
+ },
123
+ load(id) {
124
+ if (id === "\0virtual:vue-root-validator") {
125
+ const clientEntryTs = (0, node_url.fileURLToPath)(new URL("./client.ts", require("url").pathToFileURL(__filename).href));
126
+ const clientEntryJs = (0, node_url.fileURLToPath)(new URL("./client.js", require("url").pathToFileURL(__filename).href));
127
+ const clientUrl = `/@fs/${((0, node_fs.existsSync)(clientEntryTs) ? clientEntryTs : clientEntryJs).replace(/\\/g, "/")}`;
128
+ return `
129
+ // 虚拟模块:vue-root-validator
130
+ // 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成
131
+
132
+ import { setupVueRootValidator } from ${JSON.stringify(clientUrl)};
133
+
134
+ // 重新导出函数
135
+ export { setupVueRootValidator };
136
+ `;
137
+ }
138
+ return null;
139
+ }
140
+ };
141
+ }
142
+
143
+ //#endregion
144
+ module.exports = vitePluginVueRootValidator;
145
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"}
@@ -0,0 +1,72 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Vite DevServer 类型(精简版)
4
+ */
5
+ type DevServerLike = {
6
+ ws: {
7
+ send: (payload: {
8
+ type: 'error';
9
+ err: {
10
+ message: string;
11
+ stack?: string;
12
+ };
13
+ }) => void;
14
+ on: (event: string, listener: (payload: any, client: any) => void) => void;
15
+ };
16
+ config: {
17
+ logger: {
18
+ warn: (msg: string) => void;
19
+ info: (msg: string) => void;
20
+ };
21
+ };
22
+ };
23
+ /**
24
+ * Vite ResolvedConfig 类型(精简版)
25
+ */
26
+ type ResolvedConfigLike = {
27
+ command: 'serve' | 'build' | string;
28
+ };
29
+ /**
30
+ * Vite 插件:Vue Root Validator
31
+ *
32
+ * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题
33
+ *
34
+ * @returns Vite 插件对象
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * // vite.config.ts
39
+ * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
40
+ *
41
+ * export default defineConfig({
42
+ * plugins: [
43
+ * vitePluginVueRootValidator()
44
+ * ]
45
+ * });
46
+ * ```
47
+ */
48
+ declare function vitePluginVueRootValidator(): {
49
+ name: string;
50
+ apply: "serve";
51
+ /**
52
+ * 保存解析后的配置
53
+ */
54
+ configResolved(config: ResolvedConfigLike): void;
55
+ /**
56
+ * 配置开发服务器
57
+ * 监听客户端上报的警告消息,并通过 error overlay 显示
58
+ */
59
+ configureServer(server: DevServerLike): void;
60
+ /**
61
+ * 解析虚拟模块
62
+ * 处理 'virtual:vue-root-validator' 模块的导入
63
+ */
64
+ resolveId(id: string): "\0virtual:vue-root-validator" | null;
65
+ /**
66
+ * 加载虚拟模块
67
+ * 返回虚拟模块的代码内容
68
+ */
69
+ load(id: string): string | null;
70
+ };
71
+ export = vitePluginVueRootValidator;
72
+ //# sourceMappingURL=index-BIaiUtvr.d.cts.map
@@ -0,0 +1,73 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Vite DevServer 类型(精简版)
4
+ */
5
+ type DevServerLike = {
6
+ ws: {
7
+ send: (payload: {
8
+ type: 'error';
9
+ err: {
10
+ message: string;
11
+ stack?: string;
12
+ };
13
+ }) => void;
14
+ on: (event: string, listener: (payload: any, client: any) => void) => void;
15
+ };
16
+ config: {
17
+ logger: {
18
+ warn: (msg: string) => void;
19
+ info: (msg: string) => void;
20
+ };
21
+ };
22
+ };
23
+ /**
24
+ * Vite ResolvedConfig 类型(精简版)
25
+ */
26
+ type ResolvedConfigLike = {
27
+ command: 'serve' | 'build' | string;
28
+ };
29
+ /**
30
+ * Vite 插件:Vue Root Validator
31
+ *
32
+ * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题
33
+ *
34
+ * @returns Vite 插件对象
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * // vite.config.ts
39
+ * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
40
+ *
41
+ * export default defineConfig({
42
+ * plugins: [
43
+ * vitePluginVueRootValidator()
44
+ * ]
45
+ * });
46
+ * ```
47
+ */
48
+ declare function vitePluginVueRootValidator(): {
49
+ name: string;
50
+ apply: "serve";
51
+ /**
52
+ * 保存解析后的配置
53
+ */
54
+ configResolved(config: ResolvedConfigLike): void;
55
+ /**
56
+ * 配置开发服务器
57
+ * 监听客户端上报的警告消息,并通过 error overlay 显示
58
+ */
59
+ configureServer(server: DevServerLike): void;
60
+ /**
61
+ * 解析虚拟模块
62
+ * 处理 'virtual:vue-root-validator' 模块的导入
63
+ */
64
+ resolveId(id: string): "\0virtual:vue-root-validator" | null;
65
+ /**
66
+ * 加载虚拟模块
67
+ * 返回虚拟模块的代码内容
68
+ */
69
+ load(id: string): string | null;
70
+ };
71
+ //#endregion
72
+ export { vitePluginVueRootValidator as default };
73
+ //# sourceMappingURL=index-BPjVwmbE.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { existsSync } from "node:fs";
3
+
4
+ //#region src/i18n.ts
5
+ const translations = {
6
+ zh: {
7
+ messageHeader: "[vite-plugin-vue-transition-root-validator] 检测到 Vue Transition 多根节点错误",
8
+ setupInstructions: `
9
+ 如何在项目中启用此插件:
10
+
11
+ 1. 在 vite.config.ts 中添加插件:
12
+ import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
13
+
14
+ plugins: [
15
+ vitePluginVueRootValidator()
16
+ ]
17
+
18
+ 2. 在 src/main.ts 中初始化:
19
+ import { setupVueRootValidator } from 'virtual:vue-root-validator';
20
+
21
+ const app = createApp(App);
22
+ setupVueRootValidator(app, { lang: 'zh' });
23
+ app.mount('#app');
24
+ `
25
+ },
26
+ en: {
27
+ messageHeader: "[vite-plugin-vue-transition-root-validator] Vue Transition Multiple Root Nodes Error",
28
+ setupInstructions: `
29
+ How to enable this plugin in your project:
30
+
31
+ 1. Add plugin in vite.config.ts:
32
+ import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
33
+
34
+ plugins: [
35
+ vitePluginVueRootValidator()
36
+ ]
37
+
38
+ 2. Initialize in src/main.ts:
39
+ import { setupVueRootValidator } from 'virtual:vue-root-validator';
40
+
41
+ const app = createApp(App);
42
+ setupVueRootValidator(app, { lang: 'en' });
43
+ app.mount('#app');
44
+ `
45
+ }
46
+ };
47
+ function getMessageHeader(lang) {
48
+ return translations[lang].messageHeader;
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/index.ts
53
+ /**
54
+ * 发送错误覆盖层到客户端(优先定向发送,避免首屏时广播命中不到当前 client)
55
+ */
56
+ function sendErrorOverlay(args) {
57
+ const { server, message, lang, client } = args;
58
+ const payload = {
59
+ type: "error",
60
+ err: {
61
+ message: getMessageHeader(lang),
62
+ stack: message
63
+ }
64
+ };
65
+ if (client?.send) {
66
+ client.send(payload);
67
+ return;
68
+ }
69
+ server.ws.send(payload);
70
+ }
71
+ /**
72
+ * Vite 插件:Vue Root Validator
73
+ *
74
+ * 用于检测 Vue 组件在 <Transition> 内渲染时的多根节点问题
75
+ *
76
+ * @returns Vite 插件对象
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // vite.config.ts
81
+ * import vitePluginVueRootValidator from 'vite-plugin-vue-transition-root-validator';
82
+ *
83
+ * export default defineConfig({
84
+ * plugins: [
85
+ * vitePluginVueRootValidator()
86
+ * ]
87
+ * });
88
+ * ```
89
+ */
90
+ function vitePluginVueRootValidator() {
91
+ let resolved;
92
+ return {
93
+ name: "vite-plugin-vue-transition-root-validator",
94
+ apply: "serve",
95
+ configResolved(config) {
96
+ resolved = config;
97
+ },
98
+ configureServer(server) {
99
+ if (resolved.command !== "serve") return;
100
+ const lastByClient = /* @__PURE__ */ new WeakMap();
101
+ server.ws.on("vite-plugin-vue-transition-root-validator:vue-warn", (payload, client) => {
102
+ if (!payload?.message) return;
103
+ const key = payload.key ?? payload.message.slice(0, 800);
104
+ if (lastByClient.get(client) === key) {
105
+ client?.send?.("vite-plugin-vue-transition-root-validator:ack", { key });
106
+ return;
107
+ }
108
+ lastByClient.set(client, key);
109
+ const effectiveLang = payload.lang ?? "en";
110
+ sendErrorOverlay({
111
+ server,
112
+ message: payload.message,
113
+ lang: effectiveLang,
114
+ client
115
+ });
116
+ client?.send?.("vite-plugin-vue-transition-root-validator:ack", { key });
117
+ });
118
+ },
119
+ resolveId(id) {
120
+ if (id === "virtual:vue-root-validator") return "\0virtual:vue-root-validator";
121
+ return null;
122
+ },
123
+ load(id) {
124
+ if (id === "\0virtual:vue-root-validator") {
125
+ const clientEntryTs = fileURLToPath(new URL("./client.ts", import.meta.url));
126
+ const clientEntryJs = fileURLToPath(new URL("./client.js", import.meta.url));
127
+ const clientUrl = `/@fs/${(existsSync(clientEntryTs) ? clientEntryTs : clientEntryJs).replace(/\\/g, "/")}`;
128
+ return `
129
+ // 虚拟模块:vue-root-validator
130
+ // 此模块由 vite-plugin-vue-transition-root-validator 插件自动生成
131
+
132
+ import { setupVueRootValidator } from ${JSON.stringify(clientUrl)};
133
+
134
+ // 重新导出函数
135
+ export { setupVueRootValidator };
136
+ `;
137
+ }
138
+ return null;
139
+ }
140
+ };
141
+ }
142
+
143
+ //#endregion
144
+ export { vitePluginVueRootValidator as default };
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "vite-plugin-vue-transition-root-validator",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Capture Vue <Transition> runtime warnings and show actionable overlay in Vite dev.",
6
+ "license": "MIT",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ }
24
+ },
25
+ "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",
27
+ "test": "vitest run",
28
+ "typecheck": "tsc -p tsconfig.json",
29
+ "verify": "npm run build && attw --pack ."
30
+ },
31
+ "peerDependencies": {
32
+ "vite": ">=4.0.0",
33
+ "vue": ">=3.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@arethetypeswrong/cli": "^0.18.0",
37
+ "@types/node": "^22.0.0",
38
+ "publint": "^0.3.16",
39
+ "tsdown": "^0.14.2",
40
+ "typescript": "^5.8.0",
41
+ "vite": "^5.0.0",
42
+ "vitest": "^2.0.0",
43
+ "vue": "^3.0.0"
44
+ }
45
+ }