@ifds/hydration-lens-vue 0.1.0

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 ADDED
@@ -0,0 +1,27 @@
1
+ # @ifds/hydration-lens-vue
2
+
3
+ Vue 3 adapter for hydration-lens. It recognizes Vue hydration warnings, extracts the mismatch details, locates the affected DOM element when possible, and displays the shared overlay.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev @ifds/hydration-lens-vue
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Call `init()` on the client during app startup:
14
+
15
+ ```ts
16
+ import { init } from "@ifds/hydration-lens-vue";
17
+
18
+ if (import.meta.env.DEV) init();
19
+ ```
20
+
21
+ `init(options?)` is safe to call more than once and is a no-op outside the browser. Pass `{ suppressConsole: true }` to suppress recognized Vue warning output.
22
+
23
+ Supports Vue 3. For Nuxt, use [`@ifds/hydration-lens-nuxt`](../nuxt) for zero-config setup.
24
+
25
+ ## License
26
+
27
+ MIT — see the repository [LICENSE](../../LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ init: () => init,
24
+ parseVueWarning: () => parseVueWarning,
25
+ vueAdapter: () => vueAdapter
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_hydration_lens_core = require("@ifds-oss/hydration-lens-core");
29
+
30
+ // src/adapter.ts
31
+ var VUE_PREFIX = /^\[Vue warn\]:\s*/;
32
+ var TEXT_MISMATCH = /Hydration text (?:content )?mismatch/i;
33
+ var NODE_MISMATCH = /Hydration node mismatch/i;
34
+ var CHILDREN_MISMATCH = /Hydration children mismatch/i;
35
+ var COMPLETED_WITH_MISMATCHES = /Hydration completed but contains mismatches/i;
36
+ var HYDRATION_KEYWORD = /hydrat/i;
37
+ var SERVER_CLIENT_CURRENT = /rendered on server:\s*([\s\S]*?)\s*\n\s*-\s*expected on client:\s*([\s\S]*?)(?:\n|$)/i;
38
+ var SERVER_CLIENT_LEGACY = /Client:\s*"?([\s\S]*?)"?\s*\n?\s*-\s*Server:\s*"?([\s\S]*?)"?(?:\n|$)/i;
39
+ var TRAIL_TAG = /at <([A-Za-z0-9_.$]+)/g;
40
+ var issueCounter = 0;
41
+ function joinStringArgs(rawArgs) {
42
+ return rawArgs.filter((arg) => typeof arg === "string").join(" ");
43
+ }
44
+ function extractExpectedActual(message) {
45
+ const current = SERVER_CLIENT_CURRENT.exec(message);
46
+ if (current) return { expected: current[1]?.trim() ?? null, actual: current[2]?.trim() ?? null };
47
+ const legacy = SERVER_CLIENT_LEGACY.exec(message);
48
+ if (legacy) return { actual: legacy[1]?.trim() ?? null, expected: legacy[2]?.trim() ?? null };
49
+ return { expected: null, actual: null };
50
+ }
51
+ function parseComponentTrail(message) {
52
+ const trail = [];
53
+ for (const match of message.matchAll(TRAIL_TAG)) {
54
+ const name = match[1];
55
+ if (name) trail.push({ name, raw: match[0].trim() });
56
+ }
57
+ return trail;
58
+ }
59
+ function parseVueWarning(rawArgs) {
60
+ if (typeof rawArgs[0] !== "string") return null;
61
+ const message = joinStringArgs(rawArgs).replace(VUE_PREFIX, "");
62
+ if (COMPLETED_WITH_MISMATCHES.test(message)) return null;
63
+ let kind;
64
+ if (TEXT_MISMATCH.test(message)) kind = "text";
65
+ else if (NODE_MISMATCH.test(message)) kind = "node";
66
+ else if (CHILDREN_MISMATCH.test(message)) kind = "children";
67
+ else if (HYDRATION_KEYWORD.test(message)) kind = "unknown";
68
+ else return null;
69
+ const { expected, actual } = extractExpectedActual(message);
70
+ const componentTrail = parseComponentTrail(message);
71
+ const liveNode = rawArgs.slice(1).find((arg) => typeof Node !== "undefined" && arg instanceof Node) ?? null;
72
+ return { kind, rawMessage: message, expected, actual, componentTrail, targetSelectorGuess: null, liveNode };
73
+ }
74
+ var vueAdapter = {
75
+ name: "vue",
76
+ install(bus, options) {
77
+ const original = console.warn.bind(console);
78
+ console.warn = (...args) => {
79
+ const parsed = parseVueWarning(args);
80
+ if (parsed) {
81
+ bus.emit({
82
+ id: `vue-${++issueCounter}`,
83
+ timestamp: Date.now(),
84
+ framework: "vue",
85
+ ...parsed
86
+ });
87
+ if (options?.suppressConsole) return;
88
+ }
89
+ original(...args);
90
+ };
91
+ return () => {
92
+ console.warn = original;
93
+ };
94
+ }
95
+ };
96
+
97
+ // src/index.ts
98
+ var installed = false;
99
+ function init(options) {
100
+ if (typeof window === "undefined" || installed) return;
101
+ installed = true;
102
+ vueAdapter.install(import_hydration_lens_core.defaultBus, options);
103
+ (0, import_hydration_lens_core.mountOverlay)(import_hydration_lens_core.defaultBus);
104
+ }
105
+ // Annotate the CommonJS export names for ESM import in node:
106
+ 0 && (module.exports = {
107
+ init,
108
+ parseVueWarning,
109
+ vueAdapter
110
+ });
111
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/adapter.ts"],"sourcesContent":["import type { AdapterOptions } from \"@ifds-oss/hydration-lens-core\";\nimport { defaultBus, mountOverlay } from \"@ifds-oss/hydration-lens-core\";\nimport { vueAdapter } from \"./adapter.js\";\n\nlet installed = false;\n\n/** Call once on app mount. Dev-only, no-ops outside the browser and on double-init. */\nexport function init(options?: AdapterOptions): void {\n if (typeof window === \"undefined\" || installed) return;\n installed = true;\n vueAdapter.install(defaultBus, options);\n mountOverlay(defaultBus);\n}\n\nexport { vueAdapter };\nexport { parseVueWarning } from \"./adapter.js\";\n","import type { Adapter, AdapterOptions, ComponentTrailEntry, HydrationIssue, IssueBus, IssueKind } from \"@ifds-oss/hydration-lens-core\";\n\n/**\n * Vue's warn() does NOT hand console.warn a single pre-interpolated string — a real\n * capture from Vue 3.5 / Nuxt 3 shows a call like:\n *\n * console.warn(\n * \"[Vue warn]: Hydration text content mismatch on\",\n * pElement, // the live DOM node\n * \"\\n - rendered on server: abc\\n - expected on client: xyz\",\n * \"\\n\", \" at <Index\", \"onVnodeUnmounted=fn<...>\", ..., \" at <RouteProvider\", ...\n * )\n *\n * i.e. the message, the live node, and the component trace are all separate positional\n * args (mixing strings with the live vnode/DOM node), similar in spirit to how React\n * spreads its console.error args — just split differently. We join every *string* arg\n * (skipping the live node / object args) to reconstruct one message to pattern-match\n * and to extract the component trace from.\n */\nconst VUE_PREFIX = /^\\[Vue warn\\]:\\s*/;\nconst TEXT_MISMATCH = /Hydration text (?:content )?mismatch/i;\nconst NODE_MISMATCH = /Hydration node mismatch/i;\nconst CHILDREN_MISMATCH = /Hydration children mismatch/i;\nconst COMPLETED_WITH_MISMATCHES = /Hydration completed but contains mismatches/i;\nconst HYDRATION_KEYWORD = /hydrat/i;\n\n// Real (Vue 3.5) wording: \"- rendered on server: X\" / \"- expected on client: Y\".\nconst SERVER_CLIENT_CURRENT = /rendered on server:\\s*([\\s\\S]*?)\\s*\\n\\s*-\\s*expected on client:\\s*([\\s\\S]*?)(?:\\n|$)/i;\n// Older/documented wording, kept as a fallback in case another Vue version uses it:\n// \"- Client: \"X\" - Server: \"Y\"\".\nconst SERVER_CLIENT_LEGACY = /Client:\\s*\"?([\\s\\S]*?)\"?\\s*\\n?\\s*-\\s*Server:\\s*\"?([\\s\\S]*?)\"?(?:\\n|$)/i;\n\nconst TRAIL_TAG = /at <([A-Za-z0-9_.$]+)/g;\n\nlet issueCounter = 0;\n\nfunction joinStringArgs(rawArgs: unknown[]): string {\n return rawArgs.filter((arg): arg is string => typeof arg === \"string\").join(\" \");\n}\n\nfunction extractExpectedActual(message: string): { expected: string | null; actual: string | null } {\n const current = SERVER_CLIENT_CURRENT.exec(message);\n if (current) return { expected: current[1]?.trim() ?? null, actual: current[2]?.trim() ?? null };\n\n const legacy = SERVER_CLIENT_LEGACY.exec(message);\n if (legacy) return { actual: legacy[1]?.trim() ?? null, expected: legacy[2]?.trim() ?? null };\n\n return { expected: null, actual: null };\n}\n\nfunction parseComponentTrail(message: string): ComponentTrailEntry[] {\n const trail: ComponentTrailEntry[] = [];\n for (const match of message.matchAll(TRAIL_TAG)) {\n const name = match[1];\n if (name) trail.push({ name, raw: match[0].trim() });\n }\n return trail;\n}\n\nexport type ParsedVueIssue = Omit<HydrationIssue, \"id\" | \"timestamp\" | \"framework\">;\n\n/**\n * Returns null both for non-hydration console.warn calls AND for the follow-up\n * \"Hydration completed but contains mismatches.\" message — the latter just confirms\n * a mismatch already reported via one of the three shapes above, so it must not\n * be double-counted as a second issue. (In some Vue versions this follow-up is logged\n * via console.error instead, in which case this adapter — which only patches\n * console.warn — never sees it at all, which is equally safe: no double-count risk.)\n */\nexport function parseVueWarning(rawArgs: unknown[]): ParsedVueIssue | null {\n if (typeof rawArgs[0] !== \"string\") return null;\n\n const message = joinStringArgs(rawArgs).replace(VUE_PREFIX, \"\");\n if (COMPLETED_WITH_MISMATCHES.test(message)) return null;\n\n let kind: IssueKind;\n if (TEXT_MISMATCH.test(message)) kind = \"text\";\n else if (NODE_MISMATCH.test(message)) kind = \"node\";\n else if (CHILDREN_MISMATCH.test(message)) kind = \"children\";\n else if (HYDRATION_KEYWORD.test(message)) kind = \"unknown\";\n else return null;\n\n const { expected, actual } = extractExpectedActual(message);\n const componentTrail = parseComponentTrail(message);\n\n // Vue hands the live client vnode / server DOM node as a separate object argument\n // (not string-interpolated) — capture it for a free 'exact' locate, bypassing the\n // TreeWalker heuristic entirely for this issue.\n const liveNode = rawArgs.slice(1).find((arg): arg is Node => typeof Node !== \"undefined\" && arg instanceof Node) ?? null;\n\n return { kind, rawMessage: message, expected, actual, componentTrail, targetSelectorGuess: null, liveNode };\n}\n\nexport const vueAdapter: Adapter = {\n name: \"vue\",\n install(bus: IssueBus, options?: AdapterOptions) {\n const original = console.warn.bind(console);\n\n console.warn = (...args: unknown[]) => {\n const parsed = parseVueWarning(args);\n if (parsed) {\n bus.emit({\n id: `vue-${++issueCounter}`,\n timestamp: Date.now(),\n framework: \"vue\",\n ...parsed,\n });\n if (options?.suppressConsole) return; // swallow only warnings we recognized as hydration issues\n }\n original(...args);\n };\n\n return () => {\n console.warn = original;\n };\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,iCAAyC;;;ACkBzC,IAAM,aAAa;AACnB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,oBAAoB;AAC1B,IAAM,4BAA4B;AAClC,IAAM,oBAAoB;AAG1B,IAAM,wBAAwB;AAG9B,IAAM,uBAAuB;AAE7B,IAAM,YAAY;AAElB,IAAI,eAAe;AAEnB,SAAS,eAAe,SAA4B;AAClD,SAAO,QAAQ,OAAO,CAAC,QAAuB,OAAO,QAAQ,QAAQ,EAAE,KAAK,GAAG;AACjF;AAEA,SAAS,sBAAsB,SAAqE;AAClG,QAAM,UAAU,sBAAsB,KAAK,OAAO;AAClD,MAAI,QAAS,QAAO,EAAE,UAAU,QAAQ,CAAC,GAAG,KAAK,KAAK,MAAM,QAAQ,QAAQ,CAAC,GAAG,KAAK,KAAK,KAAK;AAE/F,QAAM,SAAS,qBAAqB,KAAK,OAAO;AAChD,MAAI,OAAQ,QAAO,EAAE,QAAQ,OAAO,CAAC,GAAG,KAAK,KAAK,MAAM,UAAU,OAAO,CAAC,GAAG,KAAK,KAAK,KAAK;AAE5F,SAAO,EAAE,UAAU,MAAM,QAAQ,KAAK;AACxC;AAEA,SAAS,oBAAoB,SAAwC;AACnE,QAAM,QAA+B,CAAC;AACtC,aAAW,SAAS,QAAQ,SAAS,SAAS,GAAG;AAC/C,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,KAAM,OAAM,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAYO,SAAS,gBAAgB,SAA2C;AACzE,MAAI,OAAO,QAAQ,CAAC,MAAM,SAAU,QAAO;AAE3C,QAAM,UAAU,eAAe,OAAO,EAAE,QAAQ,YAAY,EAAE;AAC9D,MAAI,0BAA0B,KAAK,OAAO,EAAG,QAAO;AAEpD,MAAI;AACJ,MAAI,cAAc,KAAK,OAAO,EAAG,QAAO;AAAA,WAC/B,cAAc,KAAK,OAAO,EAAG,QAAO;AAAA,WACpC,kBAAkB,KAAK,OAAO,EAAG,QAAO;AAAA,WACxC,kBAAkB,KAAK,OAAO,EAAG,QAAO;AAAA,MAC5C,QAAO;AAEZ,QAAM,EAAE,UAAU,OAAO,IAAI,sBAAsB,OAAO;AAC1D,QAAM,iBAAiB,oBAAoB,OAAO;AAKlD,QAAM,WAAW,QAAQ,MAAM,CAAC,EAAE,KAAK,CAAC,QAAqB,OAAO,SAAS,eAAe,eAAe,IAAI,KAAK;AAEpH,SAAO,EAAE,MAAM,YAAY,SAAS,UAAU,QAAQ,gBAAgB,qBAAqB,MAAM,SAAS;AAC5G;AAEO,IAAM,aAAsB;AAAA,EACjC,MAAM;AAAA,EACN,QAAQ,KAAe,SAA0B;AAC/C,UAAM,WAAW,QAAQ,KAAK,KAAK,OAAO;AAE1C,YAAQ,OAAO,IAAI,SAAoB;AACrC,YAAM,SAAS,gBAAgB,IAAI;AACnC,UAAI,QAAQ;AACV,YAAI,KAAK;AAAA,UACP,IAAI,OAAO,EAAE,YAAY;AAAA,UACzB,WAAW,KAAK,IAAI;AAAA,UACpB,WAAW;AAAA,UACX,GAAG;AAAA,QACL,CAAC;AACD,YAAI,SAAS,gBAAiB;AAAA,MAChC;AACA,eAAS,GAAG,IAAI;AAAA,IAClB;AAEA,WAAO,MAAM;AACX,cAAQ,OAAO;AAAA,IACjB;AAAA,EACF;AACF;;;ADhHA,IAAI,YAAY;AAGT,SAAS,KAAK,SAAgC;AACnD,MAAI,OAAO,WAAW,eAAe,UAAW;AAChD,cAAY;AACZ,aAAW,QAAQ,uCAAY,OAAO;AACtC,+CAAa,qCAAU;AACzB;","names":[]}
@@ -0,0 +1,18 @@
1
+ import { HydrationIssue, Adapter, AdapterOptions } from '@ifds-oss/hydration-lens-core';
2
+
3
+ type ParsedVueIssue = Omit<HydrationIssue, "id" | "timestamp" | "framework">;
4
+ /**
5
+ * Returns null both for non-hydration console.warn calls AND for the follow-up
6
+ * "Hydration completed but contains mismatches." message — the latter just confirms
7
+ * a mismatch already reported via one of the three shapes above, so it must not
8
+ * be double-counted as a second issue. (In some Vue versions this follow-up is logged
9
+ * via console.error instead, in which case this adapter — which only patches
10
+ * console.warn — never sees it at all, which is equally safe: no double-count risk.)
11
+ */
12
+ declare function parseVueWarning(rawArgs: unknown[]): ParsedVueIssue | null;
13
+ declare const vueAdapter: Adapter;
14
+
15
+ /** Call once on app mount. Dev-only, no-ops outside the browser and on double-init. */
16
+ declare function init(options?: AdapterOptions): void;
17
+
18
+ export { init, parseVueWarning, vueAdapter };
@@ -0,0 +1,18 @@
1
+ import { HydrationIssue, Adapter, AdapterOptions } from '@ifds-oss/hydration-lens-core';
2
+
3
+ type ParsedVueIssue = Omit<HydrationIssue, "id" | "timestamp" | "framework">;
4
+ /**
5
+ * Returns null both for non-hydration console.warn calls AND for the follow-up
6
+ * "Hydration completed but contains mismatches." message — the latter just confirms
7
+ * a mismatch already reported via one of the three shapes above, so it must not
8
+ * be double-counted as a second issue. (In some Vue versions this follow-up is logged
9
+ * via console.error instead, in which case this adapter — which only patches
10
+ * console.warn — never sees it at all, which is equally safe: no double-count risk.)
11
+ */
12
+ declare function parseVueWarning(rawArgs: unknown[]): ParsedVueIssue | null;
13
+ declare const vueAdapter: Adapter;
14
+
15
+ /** Call once on app mount. Dev-only, no-ops outside the browser and on double-init. */
16
+ declare function init(options?: AdapterOptions): void;
17
+
18
+ export { init, parseVueWarning, vueAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ // src/index.ts
2
+ import { defaultBus, mountOverlay } from "@ifds-oss/hydration-lens-core";
3
+
4
+ // src/adapter.ts
5
+ var VUE_PREFIX = /^\[Vue warn\]:\s*/;
6
+ var TEXT_MISMATCH = /Hydration text (?:content )?mismatch/i;
7
+ var NODE_MISMATCH = /Hydration node mismatch/i;
8
+ var CHILDREN_MISMATCH = /Hydration children mismatch/i;
9
+ var COMPLETED_WITH_MISMATCHES = /Hydration completed but contains mismatches/i;
10
+ var HYDRATION_KEYWORD = /hydrat/i;
11
+ var SERVER_CLIENT_CURRENT = /rendered on server:\s*([\s\S]*?)\s*\n\s*-\s*expected on client:\s*([\s\S]*?)(?:\n|$)/i;
12
+ var SERVER_CLIENT_LEGACY = /Client:\s*"?([\s\S]*?)"?\s*\n?\s*-\s*Server:\s*"?([\s\S]*?)"?(?:\n|$)/i;
13
+ var TRAIL_TAG = /at <([A-Za-z0-9_.$]+)/g;
14
+ var issueCounter = 0;
15
+ function joinStringArgs(rawArgs) {
16
+ return rawArgs.filter((arg) => typeof arg === "string").join(" ");
17
+ }
18
+ function extractExpectedActual(message) {
19
+ const current = SERVER_CLIENT_CURRENT.exec(message);
20
+ if (current) return { expected: current[1]?.trim() ?? null, actual: current[2]?.trim() ?? null };
21
+ const legacy = SERVER_CLIENT_LEGACY.exec(message);
22
+ if (legacy) return { actual: legacy[1]?.trim() ?? null, expected: legacy[2]?.trim() ?? null };
23
+ return { expected: null, actual: null };
24
+ }
25
+ function parseComponentTrail(message) {
26
+ const trail = [];
27
+ for (const match of message.matchAll(TRAIL_TAG)) {
28
+ const name = match[1];
29
+ if (name) trail.push({ name, raw: match[0].trim() });
30
+ }
31
+ return trail;
32
+ }
33
+ function parseVueWarning(rawArgs) {
34
+ if (typeof rawArgs[0] !== "string") return null;
35
+ const message = joinStringArgs(rawArgs).replace(VUE_PREFIX, "");
36
+ if (COMPLETED_WITH_MISMATCHES.test(message)) return null;
37
+ let kind;
38
+ if (TEXT_MISMATCH.test(message)) kind = "text";
39
+ else if (NODE_MISMATCH.test(message)) kind = "node";
40
+ else if (CHILDREN_MISMATCH.test(message)) kind = "children";
41
+ else if (HYDRATION_KEYWORD.test(message)) kind = "unknown";
42
+ else return null;
43
+ const { expected, actual } = extractExpectedActual(message);
44
+ const componentTrail = parseComponentTrail(message);
45
+ const liveNode = rawArgs.slice(1).find((arg) => typeof Node !== "undefined" && arg instanceof Node) ?? null;
46
+ return { kind, rawMessage: message, expected, actual, componentTrail, targetSelectorGuess: null, liveNode };
47
+ }
48
+ var vueAdapter = {
49
+ name: "vue",
50
+ install(bus, options) {
51
+ const original = console.warn.bind(console);
52
+ console.warn = (...args) => {
53
+ const parsed = parseVueWarning(args);
54
+ if (parsed) {
55
+ bus.emit({
56
+ id: `vue-${++issueCounter}`,
57
+ timestamp: Date.now(),
58
+ framework: "vue",
59
+ ...parsed
60
+ });
61
+ if (options?.suppressConsole) return;
62
+ }
63
+ original(...args);
64
+ };
65
+ return () => {
66
+ console.warn = original;
67
+ };
68
+ }
69
+ };
70
+
71
+ // src/index.ts
72
+ var installed = false;
73
+ function init(options) {
74
+ if (typeof window === "undefined" || installed) return;
75
+ installed = true;
76
+ vueAdapter.install(defaultBus, options);
77
+ mountOverlay(defaultBus);
78
+ }
79
+ export {
80
+ init,
81
+ parseVueWarning,
82
+ vueAdapter
83
+ };
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/adapter.ts"],"sourcesContent":["import type { AdapterOptions } from \"@ifds-oss/hydration-lens-core\";\nimport { defaultBus, mountOverlay } from \"@ifds-oss/hydration-lens-core\";\nimport { vueAdapter } from \"./adapter.js\";\n\nlet installed = false;\n\n/** Call once on app mount. Dev-only, no-ops outside the browser and on double-init. */\nexport function init(options?: AdapterOptions): void {\n if (typeof window === \"undefined\" || installed) return;\n installed = true;\n vueAdapter.install(defaultBus, options);\n mountOverlay(defaultBus);\n}\n\nexport { vueAdapter };\nexport { parseVueWarning } from \"./adapter.js\";\n","import type { Adapter, AdapterOptions, ComponentTrailEntry, HydrationIssue, IssueBus, IssueKind } from \"@ifds-oss/hydration-lens-core\";\n\n/**\n * Vue's warn() does NOT hand console.warn a single pre-interpolated string — a real\n * capture from Vue 3.5 / Nuxt 3 shows a call like:\n *\n * console.warn(\n * \"[Vue warn]: Hydration text content mismatch on\",\n * pElement, // the live DOM node\n * \"\\n - rendered on server: abc\\n - expected on client: xyz\",\n * \"\\n\", \" at <Index\", \"onVnodeUnmounted=fn<...>\", ..., \" at <RouteProvider\", ...\n * )\n *\n * i.e. the message, the live node, and the component trace are all separate positional\n * args (mixing strings with the live vnode/DOM node), similar in spirit to how React\n * spreads its console.error args — just split differently. We join every *string* arg\n * (skipping the live node / object args) to reconstruct one message to pattern-match\n * and to extract the component trace from.\n */\nconst VUE_PREFIX = /^\\[Vue warn\\]:\\s*/;\nconst TEXT_MISMATCH = /Hydration text (?:content )?mismatch/i;\nconst NODE_MISMATCH = /Hydration node mismatch/i;\nconst CHILDREN_MISMATCH = /Hydration children mismatch/i;\nconst COMPLETED_WITH_MISMATCHES = /Hydration completed but contains mismatches/i;\nconst HYDRATION_KEYWORD = /hydrat/i;\n\n// Real (Vue 3.5) wording: \"- rendered on server: X\" / \"- expected on client: Y\".\nconst SERVER_CLIENT_CURRENT = /rendered on server:\\s*([\\s\\S]*?)\\s*\\n\\s*-\\s*expected on client:\\s*([\\s\\S]*?)(?:\\n|$)/i;\n// Older/documented wording, kept as a fallback in case another Vue version uses it:\n// \"- Client: \"X\" - Server: \"Y\"\".\nconst SERVER_CLIENT_LEGACY = /Client:\\s*\"?([\\s\\S]*?)\"?\\s*\\n?\\s*-\\s*Server:\\s*\"?([\\s\\S]*?)\"?(?:\\n|$)/i;\n\nconst TRAIL_TAG = /at <([A-Za-z0-9_.$]+)/g;\n\nlet issueCounter = 0;\n\nfunction joinStringArgs(rawArgs: unknown[]): string {\n return rawArgs.filter((arg): arg is string => typeof arg === \"string\").join(\" \");\n}\n\nfunction extractExpectedActual(message: string): { expected: string | null; actual: string | null } {\n const current = SERVER_CLIENT_CURRENT.exec(message);\n if (current) return { expected: current[1]?.trim() ?? null, actual: current[2]?.trim() ?? null };\n\n const legacy = SERVER_CLIENT_LEGACY.exec(message);\n if (legacy) return { actual: legacy[1]?.trim() ?? null, expected: legacy[2]?.trim() ?? null };\n\n return { expected: null, actual: null };\n}\n\nfunction parseComponentTrail(message: string): ComponentTrailEntry[] {\n const trail: ComponentTrailEntry[] = [];\n for (const match of message.matchAll(TRAIL_TAG)) {\n const name = match[1];\n if (name) trail.push({ name, raw: match[0].trim() });\n }\n return trail;\n}\n\nexport type ParsedVueIssue = Omit<HydrationIssue, \"id\" | \"timestamp\" | \"framework\">;\n\n/**\n * Returns null both for non-hydration console.warn calls AND for the follow-up\n * \"Hydration completed but contains mismatches.\" message — the latter just confirms\n * a mismatch already reported via one of the three shapes above, so it must not\n * be double-counted as a second issue. (In some Vue versions this follow-up is logged\n * via console.error instead, in which case this adapter — which only patches\n * console.warn — never sees it at all, which is equally safe: no double-count risk.)\n */\nexport function parseVueWarning(rawArgs: unknown[]): ParsedVueIssue | null {\n if (typeof rawArgs[0] !== \"string\") return null;\n\n const message = joinStringArgs(rawArgs).replace(VUE_PREFIX, \"\");\n if (COMPLETED_WITH_MISMATCHES.test(message)) return null;\n\n let kind: IssueKind;\n if (TEXT_MISMATCH.test(message)) kind = \"text\";\n else if (NODE_MISMATCH.test(message)) kind = \"node\";\n else if (CHILDREN_MISMATCH.test(message)) kind = \"children\";\n else if (HYDRATION_KEYWORD.test(message)) kind = \"unknown\";\n else return null;\n\n const { expected, actual } = extractExpectedActual(message);\n const componentTrail = parseComponentTrail(message);\n\n // Vue hands the live client vnode / server DOM node as a separate object argument\n // (not string-interpolated) — capture it for a free 'exact' locate, bypassing the\n // TreeWalker heuristic entirely for this issue.\n const liveNode = rawArgs.slice(1).find((arg): arg is Node => typeof Node !== \"undefined\" && arg instanceof Node) ?? null;\n\n return { kind, rawMessage: message, expected, actual, componentTrail, targetSelectorGuess: null, liveNode };\n}\n\nexport const vueAdapter: Adapter = {\n name: \"vue\",\n install(bus: IssueBus, options?: AdapterOptions) {\n const original = console.warn.bind(console);\n\n console.warn = (...args: unknown[]) => {\n const parsed = parseVueWarning(args);\n if (parsed) {\n bus.emit({\n id: `vue-${++issueCounter}`,\n timestamp: Date.now(),\n framework: \"vue\",\n ...parsed,\n });\n if (options?.suppressConsole) return; // swallow only warnings we recognized as hydration issues\n }\n original(...args);\n };\n\n return () => {\n console.warn = original;\n };\n },\n};\n"],"mappings":";AACA,SAAS,YAAY,oBAAoB;;;ACkBzC,IAAM,aAAa;AACnB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,oBAAoB;AAC1B,IAAM,4BAA4B;AAClC,IAAM,oBAAoB;AAG1B,IAAM,wBAAwB;AAG9B,IAAM,uBAAuB;AAE7B,IAAM,YAAY;AAElB,IAAI,eAAe;AAEnB,SAAS,eAAe,SAA4B;AAClD,SAAO,QAAQ,OAAO,CAAC,QAAuB,OAAO,QAAQ,QAAQ,EAAE,KAAK,GAAG;AACjF;AAEA,SAAS,sBAAsB,SAAqE;AAClG,QAAM,UAAU,sBAAsB,KAAK,OAAO;AAClD,MAAI,QAAS,QAAO,EAAE,UAAU,QAAQ,CAAC,GAAG,KAAK,KAAK,MAAM,QAAQ,QAAQ,CAAC,GAAG,KAAK,KAAK,KAAK;AAE/F,QAAM,SAAS,qBAAqB,KAAK,OAAO;AAChD,MAAI,OAAQ,QAAO,EAAE,QAAQ,OAAO,CAAC,GAAG,KAAK,KAAK,MAAM,UAAU,OAAO,CAAC,GAAG,KAAK,KAAK,KAAK;AAE5F,SAAO,EAAE,UAAU,MAAM,QAAQ,KAAK;AACxC;AAEA,SAAS,oBAAoB,SAAwC;AACnE,QAAM,QAA+B,CAAC;AACtC,aAAW,SAAS,QAAQ,SAAS,SAAS,GAAG;AAC/C,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,KAAM,OAAM,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAYO,SAAS,gBAAgB,SAA2C;AACzE,MAAI,OAAO,QAAQ,CAAC,MAAM,SAAU,QAAO;AAE3C,QAAM,UAAU,eAAe,OAAO,EAAE,QAAQ,YAAY,EAAE;AAC9D,MAAI,0BAA0B,KAAK,OAAO,EAAG,QAAO;AAEpD,MAAI;AACJ,MAAI,cAAc,KAAK,OAAO,EAAG,QAAO;AAAA,WAC/B,cAAc,KAAK,OAAO,EAAG,QAAO;AAAA,WACpC,kBAAkB,KAAK,OAAO,EAAG,QAAO;AAAA,WACxC,kBAAkB,KAAK,OAAO,EAAG,QAAO;AAAA,MAC5C,QAAO;AAEZ,QAAM,EAAE,UAAU,OAAO,IAAI,sBAAsB,OAAO;AAC1D,QAAM,iBAAiB,oBAAoB,OAAO;AAKlD,QAAM,WAAW,QAAQ,MAAM,CAAC,EAAE,KAAK,CAAC,QAAqB,OAAO,SAAS,eAAe,eAAe,IAAI,KAAK;AAEpH,SAAO,EAAE,MAAM,YAAY,SAAS,UAAU,QAAQ,gBAAgB,qBAAqB,MAAM,SAAS;AAC5G;AAEO,IAAM,aAAsB;AAAA,EACjC,MAAM;AAAA,EACN,QAAQ,KAAe,SAA0B;AAC/C,UAAM,WAAW,QAAQ,KAAK,KAAK,OAAO;AAE1C,YAAQ,OAAO,IAAI,SAAoB;AACrC,YAAM,SAAS,gBAAgB,IAAI;AACnC,UAAI,QAAQ;AACV,YAAI,KAAK;AAAA,UACP,IAAI,OAAO,EAAE,YAAY;AAAA,UACzB,WAAW,KAAK,IAAI;AAAA,UACpB,WAAW;AAAA,UACX,GAAG;AAAA,QACL,CAAC;AACD,YAAI,SAAS,gBAAiB;AAAA,MAChC;AACA,eAAS,GAAG,IAAI;AAAA,IAClB;AAEA,WAAO,MAAM;AACX,cAAQ,OAAO;AAAA,IACjB;AAAA,EACF;AACF;;;ADhHA,IAAI,YAAY;AAGT,SAAS,KAAK,SAAgC;AACnD,MAAI,OAAO,WAAW,eAAe,UAAW;AAChD,cAAY;AACZ,aAAW,QAAQ,YAAY,OAAO;AACtC,eAAa,UAAU;AACzB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@ifds/hydration-lens-vue",
3
+ "version": "0.1.0",
4
+ "description": "Vue/Nuxt adapter for hydration-lens: patches console.warn and parses Vue hydration warnings.",
5
+ "keywords": ["vue", "nuxt", "hydration", "hydration-mismatch", "ssr", "developer-tools"],
6
+ "license": "MIT",
7
+ "repository": { "type": "git", "url": "git+https://github.com/IFDS-OSS/hydration-lens.git", "directory": "packages/vue" },
8
+ "homepage": "https://github.com/IFDS-OSS/hydration-lens#readme",
9
+ "bugs": { "url": "https://github.com/IFDS-OSS/hydration-lens/issues" },
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
17
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
18
+ }
19
+ },
20
+ "files": ["dist", "README.md"],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "peerDependencies": {
26
+ "vue": ">=3.0.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "vue": { "optional": true }
30
+ },
31
+ "dependencies": {
32
+ "@ifds/hydration-lens-core": "workspace:*"
33
+ },
34
+ "devDependencies": {
35
+ "tsup": "^8.3.5",
36
+ "typescript": "^5.7.2"
37
+ }
38
+ }