@immense/vue-pom-generator 1.0.3 → 1.0.8

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/dist/index.mjs CHANGED
@@ -2,17 +2,66 @@ import path from "node:path";
2
2
  import process from "node:process";
3
3
  import { pathToFileURL, fileURLToPath } from "node:url";
4
4
  import fs from "node:fs";
5
+ import { JSDOM } from "jsdom";
5
6
  import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression } from "@vue/compiler-core";
6
7
  import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isOptionalMemberExpression, isObjectExpression, isFile, isArrowFunctionExpression, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isObjectProperty, isProgram, VISITOR_KEYS } from "@babel/types";
7
8
  import { parseExpression, parse } from "@babel/parser";
8
- import { JSDOM } from "jsdom";
9
9
  import { performance } from "node:perf_hooks";
10
10
  import * as compilerDom from "@vue/compiler-dom";
11
- import { parse as parse$2, compile } from "@vue/compiler-dom";
11
+ import { parse as parse$2 } from "@vue/compiler-dom";
12
12
  import { parse as parse$1 } from "@vue/compiler-sfc";
13
13
  import virtualImport from "vite-plugin-virtual";
14
14
  import vue from "@vitejs/plugin-vue";
15
- import { extend } from "@vue/shared";
15
+ const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
16
+ function normalize(message) {
17
+ const trimmed = (message ?? "").trim();
18
+ if (!trimmed)
19
+ return VUE_POM_GENERATOR_LOG_PREFIX;
20
+ if (trimmed.startsWith(VUE_POM_GENERATOR_LOG_PREFIX))
21
+ return trimmed;
22
+ return `${VUE_POM_GENERATOR_LOG_PREFIX} ${trimmed}`;
23
+ }
24
+ function createLogger(options) {
25
+ const { verbosity, viteLogger } = options;
26
+ const sinkInfo = (msg) => {
27
+ if (viteLogger) {
28
+ viteLogger.info(normalize(msg));
29
+ return;
30
+ }
31
+ console.log(normalize(msg));
32
+ };
33
+ const sinkWarn = (msg) => {
34
+ if (viteLogger) {
35
+ viteLogger.warn(normalize(msg));
36
+ return;
37
+ }
38
+ console.warn(normalize(msg));
39
+ };
40
+ const sinkDebug = (msg) => {
41
+ if (viteLogger) {
42
+ viteLogger.info(normalize(msg));
43
+ return;
44
+ }
45
+ console.log(normalize(msg));
46
+ };
47
+ return {
48
+ info(message) {
49
+ if (verbosity === "silent" || verbosity === "warn")
50
+ return;
51
+ sinkInfo(message);
52
+ },
53
+ debug(message) {
54
+ if (verbosity !== "debug")
55
+ return;
56
+ sinkDebug(message);
57
+ },
58
+ warn(message) {
59
+ if (verbosity === "silent")
60
+ return;
61
+ sinkWarn(message);
62
+ }
63
+ };
64
+ }
16
65
  const INDENT = " ";
17
66
  const INDENT2 = `${INDENT}${INDENT}`;
18
67
  const INDENT3 = `${INDENT2}${INDENT}`;
@@ -147,10 +196,21 @@ ${INDENT}}
147
196
  `;
148
197
  return content;
149
198
  }
199
+ function isAllDigits(value) {
200
+ if (!value)
201
+ return false;
202
+ for (let i = 0; i < value.length; i++) {
203
+ const code = value.charCodeAt(i);
204
+ if (code < 48 || code > 57)
205
+ return false;
206
+ }
207
+ return true;
208
+ }
150
209
  function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
151
210
  const roleSuffix = upperFirst$1(nativeRole || "Element");
152
211
  const baseName = upperFirst$1(methodName);
153
- const hasRoleSuffix = baseName.endsWith(roleSuffix) || new RegExp(`^${roleSuffix}\\d+$`).test(baseName);
212
+ const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
213
+ const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
154
214
  const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
155
215
  const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
156
216
  if (needsKey) {
@@ -456,6 +516,23 @@ function tryGetClickDirective(node) {
456
516
  function nodeHasClickDirective(node) {
457
517
  return tryGetClickDirective(node) !== void 0;
458
518
  }
519
+ function getTemplateSlotScope(node) {
520
+ if (node.tag !== "template") {
521
+ return null;
522
+ }
523
+ const slotProp = node.props.find((prop) => {
524
+ return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot";
525
+ });
526
+ if (slotProp?.exp) {
527
+ if (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
528
+ return slotProp.exp.content;
529
+ }
530
+ if (slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) {
531
+ return stringifyExpression(slotProp.exp);
532
+ }
533
+ }
534
+ return null;
535
+ }
459
536
  function nodeHasToDirective(node) {
460
537
  const toDirective = findDirectiveByName(node, "bind", "to");
461
538
  if (toDirective?.exp) {
@@ -525,6 +602,37 @@ function getIdOrName(node) {
525
602
  }
526
603
  return identifier;
527
604
  }
605
+ function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
606
+ let parent = getParent(hierarchyMap2, node);
607
+ while (parent) {
608
+ if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
609
+ const scope = getTemplateSlotScope(parent);
610
+ if (scope) {
611
+ let key = scope.trim();
612
+ if (key.startsWith("{") && key.endsWith("}")) {
613
+ const inner = key.slice(1, -1).trim();
614
+ let cutIdx = -1;
615
+ const commaIdx = inner.indexOf(",");
616
+ const colonIdx = inner.indexOf(":");
617
+ if (commaIdx !== -1 && colonIdx !== -1) {
618
+ cutIdx = Math.min(commaIdx, colonIdx);
619
+ } else if (commaIdx !== -1) {
620
+ cutIdx = commaIdx;
621
+ } else if (colonIdx !== -1) {
622
+ cutIdx = colonIdx;
623
+ }
624
+ const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
625
+ if (first) {
626
+ key = first;
627
+ }
628
+ }
629
+ return key;
630
+ }
631
+ }
632
+ parent = getParent(hierarchyMap2, parent);
633
+ }
634
+ return null;
635
+ }
528
636
  function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
529
637
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
530
638
  return null;
@@ -614,6 +722,9 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
614
722
  function getParent(hierarchyMap2, node) {
615
723
  return hierarchyMap2.get(node) || null;
616
724
  }
725
+ function nodeHandlerAttributeValue(node) {
726
+ return nodeHandlerAttributeInfo(node)?.semanticNameHint ?? null;
727
+ }
617
728
  function nodeHandlerAttributeInfo(node) {
618
729
  const handlerDirective = findDirectiveByName(node, "bind", "handler");
619
730
  if (!handlerDirective?.exp) {
@@ -972,7 +1083,7 @@ function generateToDirectiveDataTestId(componentName, node, toDirective, context
972
1083
  }
973
1084
  const source = stringifyExpression(toDirective.exp);
974
1085
  const toAst = toDirective.exp.ast;
975
- const interpolated = !(toAst == null || toAst === false || toAst) && isTemplateLiteral(toAst);
1086
+ const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && isTemplateLiteral(toAst);
976
1087
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
977
1088
  } else {
978
1089
  const innerText = getInnerText(node);
@@ -1033,7 +1144,7 @@ function toDirectiveObjectFieldNameValue(node) {
1033
1144
  return null;
1034
1145
  }
1035
1146
  }
1036
- function getComposedClickHandlerContent(node, _context, _innerText, clickDirective, _options = {}) {
1147
+ function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1037
1148
  const click = clickDirective ?? tryGetClickDirective(node);
1038
1149
  if (!click) {
1039
1150
  return "";
@@ -1052,8 +1163,9 @@ function getComposedClickHandlerContent(node, _context, _innerText, clickDirecti
1052
1163
  }
1053
1164
  }
1054
1165
  }
1055
- handlerName = normalizeHandlerName(handlerName);
1056
- const normalizedHandlerSegment = handlerName ? `-${toPascalCase(handlerName)}` : "";
1166
+ const hName = normalizeHandlerName(handlerName);
1167
+ const name = hName || innerText || "";
1168
+ const normalizedHandlerSegment = name ? `-${toPascalCase(name)}` : "";
1057
1169
  const result = normalizedHandlerSegment;
1058
1170
  return result.replace(/[^a-z-]/gi, "");
1059
1171
  }
@@ -1349,6 +1461,31 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
1349
1461
  if (!raw) {
1350
1462
  return null;
1351
1463
  }
1464
+ try {
1465
+ const ast2 = parseExpression(raw, { plugins: ["typescript"] });
1466
+ if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
1467
+ const tl = ast2;
1468
+ const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
1469
+ const expressionCount = (tl.expressions ?? []).length;
1470
+ const isStatic = expressionCount === 0;
1471
+ const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
1472
+ if (isStatic) {
1473
+ return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
1474
+ }
1475
+ return {
1476
+ value: unwrappedTemplate,
1477
+ isDynamic: true,
1478
+ isStaticLiteral: false,
1479
+ template: unwrappedTemplate,
1480
+ templateExpressionCount: expressionCount
1481
+ };
1482
+ }
1483
+ if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
1484
+ const sl = ast2;
1485
+ return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
1486
+ }
1487
+ } catch {
1488
+ }
1352
1489
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
1353
1490
  }
1354
1491
  function isTemplatePlaceholder(part) {
@@ -1476,13 +1613,15 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1476
1613
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
1477
1614
  );
1478
1615
  }
1479
- if (args.bestKeyPlaceholder && !existing.template.includes(args.bestKeyPlaceholder)) {
1616
+ const hasExact = args.bestKeyPlaceholder && existing.template.includes(args.bestKeyPlaceholder);
1617
+ const hasVarAccess = args.bestKeyVariable && existing.template.includes(args.bestKeyVariable);
1618
+ if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
1480
1619
  throw new Error(
1481
1620
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
1482
1621
  Component: ${args.componentName}
1483
1622
  File: ${file}:${locationHint}
1484
1623
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1485
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}
1624
+ Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
1486
1625
 
1487
1626
  Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
1488
1627
  );
@@ -1507,7 +1646,7 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
1507
1646
  Component: ${args.componentName}
1508
1647
  File: ${file}:${locationHint}
1509
1648
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1510
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}
1649
+ Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
1511
1650
 
1512
1651
  Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
1513
1652
  );
@@ -1534,6 +1673,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
1534
1673
  case "checkbox":
1535
1674
  case "toggle":
1536
1675
  case "radio":
1676
+ case "grid":
1537
1677
  return role;
1538
1678
  default:
1539
1679
  return void 0;
@@ -1628,10 +1768,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
1628
1768
  args.dependencies.__pomPrimaryByGetterName ??= /* @__PURE__ */ new Map();
1629
1769
  const primaryByActionName = args.dependencies.__pomPrimaryByActionName;
1630
1770
  const hintCandidates = (() => {
1631
- const baseHints = [args.semanticNameHint];
1632
- if (nameCollisionBehavior === "error") {
1633
- baseHints.push(...args.semanticNameHintAlternates ?? []);
1634
- }
1771
+ const baseHints = [
1772
+ args.semanticNameHint,
1773
+ ...args.semanticNameHintAlternates ?? []
1774
+ ];
1635
1775
  const out = [];
1636
1776
  const seen = /* @__PURE__ */ new Set();
1637
1777
  for (const h of baseHints) {
@@ -2380,6 +2520,84 @@ async function ensureDomShim() {
2380
2520
  if (!g.requestAnimationFrame)
2381
2521
  g.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
2382
2522
  }
2523
+ async function introspectNuxtPages(projectRoot) {
2524
+ const possiblePagesDirs = ["app/pages", "pages"];
2525
+ let pagesDir = "";
2526
+ for (const dir of possiblePagesDirs) {
2527
+ const abs = path.resolve(projectRoot, dir);
2528
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
2529
+ pagesDir = abs;
2530
+ break;
2531
+ }
2532
+ }
2533
+ if (!pagesDir) {
2534
+ debugLog(`[router-introspection][nuxt] Could not find pages directory in ${projectRoot}`);
2535
+ return { routeNameMap: /* @__PURE__ */ new Map(), routePathMap: /* @__PURE__ */ new Map(), routeMetaEntries: [] };
2536
+ }
2537
+ const routeMetaEntries = [];
2538
+ const walk = (dir, baseRoute) => {
2539
+ const files = fs.readdirSync(dir);
2540
+ for (const file of files) {
2541
+ const fullPath = path.join(dir, file);
2542
+ const stat = fs.statSync(fullPath);
2543
+ if (stat.isDirectory()) {
2544
+ walk(fullPath, `${baseRoute}/${file}`);
2545
+ continue;
2546
+ }
2547
+ if (!file.endsWith(".vue"))
2548
+ continue;
2549
+ const componentName = file.slice(0, -4);
2550
+ if (componentName === "index" && baseRoute === "") {
2551
+ routeMetaEntries.push({
2552
+ componentName: "index",
2553
+ pathTemplate: "/",
2554
+ params: [],
2555
+ query: []
2556
+ });
2557
+ continue;
2558
+ }
2559
+ let routePath = componentName === "index" ? baseRoute : `${baseRoute}/${componentName}`;
2560
+ if (!routePath.startsWith("/"))
2561
+ routePath = `/${routePath}`;
2562
+ const params = [];
2563
+ let pathTemplate = "";
2564
+ for (let i = 0; i < routePath.length; i++) {
2565
+ const ch = routePath[i];
2566
+ if (ch !== "[") {
2567
+ pathTemplate += ch;
2568
+ continue;
2569
+ }
2570
+ let name = "";
2571
+ i++;
2572
+ while (i < routePath.length) {
2573
+ const c = routePath[i];
2574
+ if (c === "]")
2575
+ break;
2576
+ name += c;
2577
+ i++;
2578
+ }
2579
+ if (name) {
2580
+ params.push({ name, optional: false });
2581
+ pathTemplate += `:${name}`;
2582
+ } else {
2583
+ pathTemplate += "[]";
2584
+ }
2585
+ }
2586
+ routeMetaEntries.push({
2587
+ componentName,
2588
+ pathTemplate,
2589
+ params,
2590
+ query: []
2591
+ });
2592
+ }
2593
+ };
2594
+ walk(pagesDir, "");
2595
+ return {
2596
+ routeNameMap: /* @__PURE__ */ new Map(),
2597
+ routePathMap: /* @__PURE__ */ new Map(),
2598
+ routeMetaEntries
2599
+ };
2600
+ }
2383
2601
  async function parseRouterFileFromCwd(routerEntryPath) {
2384
2602
  return await runRouterIntrospectionExclusive(async () => {
2385
2603
  const routerEntry = path.resolve(routerEntryPath);
@@ -2495,9 +2713,9 @@ function resolveRouterEntry(projectRoot, routerEntry) {
2495
2713
  const root = projectRoot ?? process.cwd();
2496
2714
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
2497
2715
  }
2498
- async function getRouteMetaByComponent(projectRoot, routerEntry) {
2499
- const resolvedRouterEntry = resolveRouterEntry(projectRoot, routerEntry);
2500
- const { routeMetaEntries } = await parseRouterFileFromCwd(resolvedRouterEntry);
2716
+ async function getRouteMetaByComponent(projectRoot, routerEntry, routerType) {
2717
+ const root = projectRoot ?? process.cwd();
2718
+ const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry));
2501
2719
  const map = /* @__PURE__ */ new Map();
2502
2720
  for (const entry of routeMetaEntries) {
2503
2721
  const list = map.get(entry.componentName) ?? [];
@@ -2650,12 +2868,14 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
2650
2868
  customPomImportAliases,
2651
2869
  testIdAttribute,
2652
2870
  emitLanguages: emitLanguagesOverride,
2871
+ csharp,
2653
2872
  vueRouterFluentChaining,
2654
- routerEntry
2873
+ routerEntry,
2874
+ routerType
2655
2875
  } = options;
2656
2876
  const emitLanguages = emitLanguagesOverride?.length ? emitLanguagesOverride : ["ts"];
2657
2877
  const outDir = outDirOverride ?? "./pom";
2658
- const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry) : void 0;
2878
+ const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType) : void 0;
2659
2879
  if (emitLanguages.includes("ts")) {
2660
2880
  const files = await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
2661
2881
  customPomAttachments,
@@ -2677,7 +2897,8 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
2677
2897
  }
2678
2898
  if (emitLanguages.includes("csharp")) {
2679
2899
  const csFiles = generateAggregatedCSharpFiles(componentHierarchyMap, outDir, {
2680
- projectRoot
2900
+ testIdAttribute,
2901
+ csharp
2681
2902
  });
2682
2903
  for (const file of csFiles) {
2683
2904
  createFile(file.filePath, file.content);
@@ -2700,11 +2921,11 @@ function toCSharpParam(paramTypeExpr) {
2700
2921
  const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
2701
2922
  const typePart = left.includes("|") ? "string" : left;
2702
2923
  let type = "string";
2703
- if (/(^|\s)boolean(\s|$)/.test(typePart))
2924
+ if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
2704
2925
  type = "bool";
2705
- else if (/(^|\s)string(\s|$)/.test(typePart))
2926
+ else if (/(?:^|\s)string(?:\s|$)/.test(typePart))
2706
2927
  type = "string";
2707
- else if (/(^|\s)number(\s|$)/.test(typePart))
2928
+ else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
2708
2929
  type = "int";
2709
2930
  else if (/\d+/.test(typePart) && typePart === "")
2710
2931
  type = "int";
@@ -2741,8 +2962,9 @@ function formatCSharpParams(params) {
2741
2962
  return { signature: signatureParts.join(", "), argNames };
2742
2963
  }
2743
2964
  function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options = {}) {
2744
- const projectRoot = options.projectRoot ?? process.cwd();
2745
- const outAbs = path.isAbsolute(outDir) ? outDir : path.resolve(projectRoot, outDir);
2965
+ const outAbs = ensureDir(outDir);
2966
+ const namespace = options.csharp?.namespace ?? "Playwright.Generated";
2967
+ const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
2746
2968
  const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
2747
2969
  const header = [
2748
2970
  "// <auto-generated>",
@@ -2756,13 +2978,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
2756
2978
  "using System.Threading.Tasks;",
2757
2979
  "using Microsoft.Playwright;",
2758
2980
  "",
2759
- "namespace ImmyBot.Playwright.Generated;",
2981
+ `namespace ${namespace};`,
2760
2982
  "",
2761
- "public abstract class BasePage",
2983
+ "public abstract partial class BasePage",
2762
2984
  "{",
2763
2985
  " protected BasePage(IPage page) => Page = page;",
2764
2986
  " protected IPage Page { get; }",
2765
- " protected ILocator LocatorByTestId(string testId) => Page.GetByTestId(testId);",
2987
+ ` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
2766
2988
  "",
2767
2989
  " // Minimal vue-select helper mirroring the TS BasePage.selectVSelectByTestId behavior.",
2768
2990
  " // Note: annotationText is currently a no-op in C# output (we don't render a cursor overlay).",
@@ -2786,10 +3008,11 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
2786
3008
  ].join("\n");
2787
3009
  const chunks = [header];
2788
3010
  for (const [componentName, deps] of entries) {
3011
+ const className = toPascalCaseLocal(componentName);
2789
3012
  chunks.push(
2790
- `public sealed class ${componentName} : BasePage
3013
+ `public partial class ${className} : BasePage
2791
3014
  {
2792
- public ${componentName}(IPage page) : base(page) { }
3015
+ public ${className}(IPage page) : base(page) { }
2793
3016
  `
2794
3017
  );
2795
3018
  const primaries = Array.from(deps.dataTestIdSet ?? []).map((e) => ({ pom: e.pom, target: e.targetPageObjectModelClass })).filter((x) => !!x.pom).sort((a, b) => a.pom.methodName.localeCompare(b.pom.methodName));
@@ -2928,7 +3151,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
2928
3151
  return [{ filePath: outputFile, content: chunks.join("\n") }];
2929
3152
  }
2930
3153
  function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
2931
- const { generateFixtures, pomOutDir, projectRoot } = options;
3154
+ const { generateFixtures, pomOutDir } = options;
2932
3155
  if (!generateFixtures)
2933
3156
  return;
2934
3157
  const defaultFixtureOutDirRel = pomOutDir;
@@ -2936,7 +3159,7 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
2936
3159
  const looksLikeFilePath = fixtureOutRel.endsWith(".ts") || fixtureOutRel.endsWith(".tsx") || fixtureOutRel.endsWith(".mts") || fixtureOutRel.endsWith(".cts");
2937
3160
  const fixtureOutDirRel = looksLikeFilePath ? path.dirname(fixtureOutRel) : fixtureOutRel;
2938
3161
  const fixtureFileName = looksLikeFilePath ? path.basename(fixtureOutRel) : "fixtures.g.ts";
2939
- const root = projectRoot ?? process.cwd();
3162
+ const root = options.projectRoot ?? process.cwd();
2940
3163
  const fixtureOutDirAbs = path.isAbsolute(fixtureOutDirRel) ? fixtureOutDirRel : path.resolve(root, fixtureOutDirRel);
2941
3164
  const pomDirAbs = path.isAbsolute(pomOutDir) ? pomOutDir : path.resolve(root, pomOutDir);
2942
3165
  const pomImport = toPosixRelativePath(fixtureOutDirAbs, pomDirAbs);
@@ -3177,7 +3400,7 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3177
3400
  const views = entries.filter(([, d]) => d.isView);
3178
3401
  const components = entries.filter(([, d]) => !d.isView);
3179
3402
  const makeAggregatedContent = (header2, outputDir, items) => {
3180
- const imports = ['import type { Locator as PwLocator, Page as PwPage } from "@playwright/test";'];
3403
+ const imports = [];
3181
3404
  if (!basePageClassPath) {
3182
3405
  throw new Error("basePageClassPath is required for aggregated generation");
3183
3406
  }
@@ -3190,6 +3413,16 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3190
3413
  " err?: string;",
3191
3414
  "}"
3192
3415
  ].join("\n");
3416
+ const inlinePlaywrightTypesModule = () => {
3417
+ const typesPath = fileURLToPath(new URL("./playwright-types.ts", import.meta.url));
3418
+ let typesSource = "";
3419
+ try {
3420
+ typesSource = fs.readFileSync(typesPath, "utf8");
3421
+ } catch {
3422
+ throw new Error(`Failed to read playwright-types.ts at ${typesPath}`);
3423
+ }
3424
+ return typesSource.trim();
3425
+ };
3193
3426
  const inlinePointerModule = () => {
3194
3427
  const pointerPath = fileURLToPath(new URL("./Pointer.ts", import.meta.url));
3195
3428
  let pointerSource = "";
@@ -3212,6 +3445,10 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3212
3445
  /import\s+type\s*\{\s*Locator\s+as\s+PwLocator\s*,\s*Page\s+as\s+PwPage\s*\}\s*from\s*["']@playwright\/test["'];?\s*/,
3213
3446
  ""
3214
3447
  );
3448
+ pointerSource = pointerSource.replace(
3449
+ /import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
3450
+ ""
3451
+ );
3215
3452
  return pointerSource.trim();
3216
3453
  };
3217
3454
  const inlineBasePageModule = () => {
@@ -3236,18 +3473,23 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3236
3473
  ""
3237
3474
  );
3238
3475
  basePageSource = basePageSource.replace(
3239
- /import\s*\{\s*Pointer\s*\}\s*from\s*["']\.\/Pointer["'];?\s*/g,
3476
+ /import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
3477
+ ""
3478
+ );
3479
+ basePageSource = basePageSource.replace(
3480
+ /import\s+(?:type\s*)?\{[\s\S]*?\}\s*from\s*["']\.\/Pointer["'];?\s*/g,
3240
3481
  ""
3241
3482
  );
3242
3483
  return basePageSource.trim();
3243
3484
  };
3485
+ const playwrightTypesInline = inlinePlaywrightTypesModule();
3244
3486
  const pointerInline = inlinePointerModule();
3245
3487
  const basePageInline = inlineBasePageModule();
3246
3488
  const addCustomPomImports = () => {
3247
3489
  const importAliases = {
3248
3490
  Toggle: "ToggleWidget",
3249
3491
  Checkbox: "CheckboxWidget",
3250
- ...options.customPomImportAliases ?? {}
3492
+ ...options.customPomImportAliases
3251
3493
  };
3252
3494
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
3253
3495
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
@@ -3430,6 +3672,8 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3430
3672
  header2,
3431
3673
  ...imports,
3432
3674
  "",
3675
+ playwrightTypesInline,
3676
+ "",
3433
3677
  pointerInline,
3434
3678
  "",
3435
3679
  basePageInline,
@@ -3441,7 +3685,8 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
3441
3685
  };
3442
3686
  const base = ensureDir(outDir);
3443
3687
  const outputFile = path.join(base, "page-object-models.g.ts");
3444
- const header = `${eslintSuppressionHeader}/**
3688
+ const header = `/// <reference lib="es2015" />
3689
+ ${eslintSuppressionHeader}/**
3445
3690
  * Aggregated generated POMs
3446
3691
  ${AUTO_GENERATED_COMMENT}`;
3447
3692
  const content = makeAggregatedContent(header, path.dirname(outputFile), [...views, ...components]);
@@ -3588,6 +3833,7 @@ function createBuildProcessorPlugin(options) {
3588
3833
  normalizedBasePagePath,
3589
3834
  outDir,
3590
3835
  emitLanguages,
3836
+ csharp,
3591
3837
  generateFixtures,
3592
3838
  customPomAttachments,
3593
3839
  projectRootRef,
@@ -3596,6 +3842,7 @@ function createBuildProcessorPlugin(options) {
3596
3842
  testIdAttribute,
3597
3843
  routerAwarePoms,
3598
3844
  resolvedRouterEntry,
3845
+ routerType,
3599
3846
  loggerRef
3600
3847
  } = options;
3601
3848
  let lastGeneratedEntryCount = 0;
@@ -3610,9 +3857,15 @@ function createBuildProcessorPlugin(options) {
3610
3857
  setResolveToComponentNameFn(() => null);
3611
3858
  return;
3612
3859
  }
3613
- if (!resolvedRouterEntry)
3614
- throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
3615
- const { routeNameMap, routePathMap } = await parseRouterFileFromCwd(resolvedRouterEntry);
3860
+ let result;
3861
+ if (routerType === "nuxt") {
3862
+ result = await introspectNuxtPages(projectRootRef.current);
3863
+ } else {
3864
+ if (!resolvedRouterEntry)
3865
+ throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
3866
+ result = await parseRouterFileFromCwd(resolvedRouterEntry);
3867
+ }
3868
+ const { routeNameMap, routePathMap } = result;
3616
3869
  setRouteNameToComponentNameMap(routeNameMap);
3617
3870
  setResolveToComponentNameFn((to) => {
3618
3871
  if (typeof to === "string") {
@@ -3649,6 +3902,7 @@ function createBuildProcessorPlugin(options) {
3649
3902
  generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
3650
3903
  outDir,
3651
3904
  emitLanguages,
3905
+ csharp,
3652
3906
  generateFixtures,
3653
3907
  customPomAttachments,
3654
3908
  projectRoot: projectRootRef.current,
@@ -3656,7 +3910,8 @@ function createBuildProcessorPlugin(options) {
3656
3910
  customPomImportAliases,
3657
3911
  testIdAttribute,
3658
3912
  vueRouterFluentChaining: routerAwarePoms,
3659
- routerEntry: resolvedRouterEntry
3913
+ routerEntry: resolvedRouterEntry,
3914
+ routerType
3660
3915
  });
3661
3916
  lastGeneratedEntryCount = entryCount;
3662
3917
  loggerRef.current.info(`generated POMs (${entryCount} entries)`);
@@ -3785,7 +4040,7 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
3785
4040
  return null;
3786
4041
  }
3787
4042
  }
3788
- function tryInferNativeWrapperRoleFromSfc(tag) {
4043
+ function tryInferNativeWrapperRoleFromSfc(tag, vueFilesPathMap) {
3789
4044
  const first = tag.charCodeAt(0);
3790
4045
  const isUpper = first >= 65 && first <= 90;
3791
4046
  if (!isUpper)
@@ -3794,8 +4049,14 @@ function tryInferNativeWrapperRoleFromSfc(tag) {
3794
4049
  if (cached)
3795
4050
  return cached;
3796
4051
  const candidates = [
3797
- path.resolve(process.cwd(), "src/components", `${tag}.vue`)
4052
+ path.resolve(process.cwd(), "src/components", `${tag}.vue`),
4053
+ path.resolve(process.cwd(), "components", `${tag}.vue`),
4054
+ path.resolve(process.cwd(), "app/components", `${tag}.vue`)
3798
4055
  ];
4056
+ const registeredPath = vueFilesPathMap?.get(tag);
4057
+ if (registeredPath) {
4058
+ candidates.unshift(path.resolve(process.cwd(), registeredPath));
4059
+ }
3799
4060
  const filePath = candidates.find((p) => fs.existsSync(p));
3800
4061
  if (!filePath) {
3801
4062
  inferredNativeWrapperConfigByTag.set(tag, { role: "" });
@@ -3836,14 +4097,22 @@ function tryInferNativeWrapperRoleFromSfc(tag) {
3836
4097
  inferredNativeWrapperConfigByTag.set(tag, { role: "" });
3837
4098
  return null;
3838
4099
  }
3839
- if (rootTag === "input" || rootTag === "textarea") {
4100
+ if (rootTag === "input" || rootTag === "textarea" || rootTag === "uinput" || rootTag === "utextarea") {
3840
4101
  inferredNativeWrapperConfigByTag.set(tag, { role: "input" });
3841
4102
  return { role: "input" };
3842
4103
  }
3843
- if (rootTag === "select") {
4104
+ if (rootTag === "select" || rootTag === "uselect") {
3844
4105
  inferredNativeWrapperConfigByTag.set(tag, { role: "select" });
3845
4106
  return { role: "select" };
3846
4107
  }
4108
+ if (rootTag === "vselect") {
4109
+ inferredNativeWrapperConfigByTag.set(tag, { role: "vselect" });
4110
+ return { role: "vselect" };
4111
+ }
4112
+ if (rootTag === "button" || rootTag === "ubutton") {
4113
+ inferredNativeWrapperConfigByTag.set(tag, { role: "button" });
4114
+ return { role: "button" };
4115
+ }
3847
4116
  inferredNativeWrapperConfigByTag.set(tag, { role: "" });
3848
4117
  return null;
3849
4118
  }
@@ -4018,6 +4287,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4018
4287
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
4019
4288
  const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
4020
4289
  const warn = options.warn;
4290
+ const vueFilesPathMap = options.vueFilesPathMap;
4021
4291
  const safeRealpath = (p) => {
4022
4292
  try {
4023
4293
  return fs.existsSync(p) ? fs.realpathSync(p) : p;
@@ -4069,18 +4339,14 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4069
4339
  const parentElement = !parentIsRoot && context?.parent?.type === NodeTypes.ELEMENT ? context.parent : null;
4070
4340
  hierarchyMap.set(element, parentElement);
4071
4341
  const normalizeFilePath = (filePath) => path.normalize(safeRealpath(path.resolve(filePath)));
4072
- const getParentComponentName = () => {
4073
- const normalizedFilePath2 = normalizeFilePath(context.filename);
4074
- return path.basename(normalizedFilePath2, ".vue");
4075
- };
4076
- const parentComponentName = getParentComponentName();
4077
4342
  const normalizedFilePath = normalizeFilePath(context.filename);
4078
- const relToViewsDir = path.relative(normalizedViewsDirAbs, normalizedFilePath);
4079
- const isView = !relToViewsDir.startsWith("..") && !path.isAbsolute(relToViewsDir);
4080
- const ensureDependencies = (parentComponentName2) => {
4081
- let dependencies2 = componentHierarchyMap.get(parentComponentName2);
4082
- if (!dependencies2) {
4083
- dependencies2 = {
4343
+ const parentComponentName = componentName;
4344
+ const dependencies = (() => {
4345
+ let deps = componentHierarchyMap.get(componentName);
4346
+ if (!deps) {
4347
+ const relToViewsDir = path.relative(normalizedViewsDirAbs, normalizedFilePath);
4348
+ const isView = !relToViewsDir.startsWith("..") && !path.isAbsolute(relToViewsDir);
4349
+ deps = {
4084
4350
  filePath: context.filename,
4085
4351
  childrenComponentSet: /* @__PURE__ */ new Set(),
4086
4352
  usedComponentSet: /* @__PURE__ */ new Set(),
@@ -4088,11 +4354,10 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4088
4354
  isView,
4089
4355
  methodsContent: ""
4090
4356
  };
4091
- componentHierarchyMap.set(parentComponentName2, dependencies2);
4357
+ componentHierarchyMap.set(componentName, deps);
4092
4358
  }
4093
- return dependencies2;
4094
- };
4095
- const dependencies = ensureDependencies(parentComponentName);
4359
+ return deps;
4360
+ })();
4096
4361
  const isComponentLikeTag = (tag) => {
4097
4362
  if (!tag) return false;
4098
4363
  const first = tag.charCodeAt(0);
@@ -4103,13 +4368,24 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4103
4368
  dependencies.usedComponentSet.add(element.tag);
4104
4369
  }
4105
4370
  if (!nativeWrappers[element.tag]) {
4106
- const inferred = tryInferNativeWrapperRoleFromSfc(element.tag);
4371
+ const inferred = tryInferNativeWrapperRoleFromSfc(element.tag, vueFilesPathMap);
4107
4372
  if (inferred?.role) {
4108
4373
  nativeWrappers[element.tag] = { role: inferred.role };
4374
+ } else if (element.tag.endsWith("Button") || element.tag === "AylaButton") {
4375
+ nativeWrappers[element.tag] = { role: "button" };
4376
+ } else if (element.tag === "DxDataGrid") {
4377
+ nativeWrappers[element.tag] = { role: "grid" };
4109
4378
  }
4110
4379
  }
4111
- const getBestAvailableKeyValue = () => getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
4112
- const bestKeyPlaceholder = getBestAvailableKeyValue();
4380
+ const getBestAvailableKeyValue = () => {
4381
+ const vForKey = getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
4382
+ if (vForKey) return vForKey;
4383
+ return getContainedInSlotDataKeyValue(element, hierarchyMap);
4384
+ };
4385
+ const bestKeyInferred = getBestAvailableKeyValue();
4386
+ const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
4387
+ const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
4388
+ const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
4113
4389
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
4114
4390
  const parentKey = !parentIsRoot && context?.parent ? context.parent : null;
4115
4391
  const conditional = getConditionalDirectiveInfo(element);
@@ -4194,6 +4470,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4194
4470
  nativeRole,
4195
4471
  preferredGeneratedValue: args.preferredGeneratedValue,
4196
4472
  bestKeyPlaceholder,
4473
+ bestKeyVariable,
4197
4474
  keyValuesOverride,
4198
4475
  entryOverrides: args.entryOverrides,
4199
4476
  semanticNameHint: args.semanticNameHint,
@@ -4222,16 +4499,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4222
4499
  });
4223
4500
  return;
4224
4501
  }
4502
+ const innerText = getInnerText(element) || null;
4225
4503
  const toDirective = nodeHasToDirective(element);
4226
4504
  if (toDirective) {
4227
4505
  const dataTestId = generateToDirectiveDataTestId(componentName, element, toDirective, context, hierarchyMap, nativeWrappers);
4228
4506
  const target = tryResolveToDirectiveTargetComponentName(toDirective);
4229
4507
  const routeNameHint = toDirectiveObjectFieldNameValue(toDirective);
4230
- const semanticNameHint2 = routeNameHint || target || conditionalHint || void 0;
4508
+ const existing = tryGetExistingElementDataTestId(element, testIdAttribute);
4509
+ const semanticNameHint2 = routeNameHint || target || void 0;
4510
+ const alternates = (target ? [] : [innerText, existing?.value, conditionalHint]).filter(Boolean);
4231
4511
  const rawTo = (toDirective.exp?.loc?.source ?? "").trim();
4232
4512
  const pomMergeKey = routeNameHint ? `to:name:${routeNameHint}` : rawTo ? `to:expr:${rawTo}` : void 0;
4233
- const existing = tryGetExistingElementDataTestId(element, testIdAttribute);
4234
- const preferredGeneratedValue = dataTestId ?? (existing ? staticAttributeValue(existing.value) : null);
4513
+ const preferredGeneratedValue = dataTestId ?? (existing ? existing.isDynamic ? templateAttributeValue(existing.template) : staticAttributeValue(existing.value) : null);
4235
4514
  if (!preferredGeneratedValue) {
4236
4515
  return;
4237
4516
  }
@@ -4239,6 +4518,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4239
4518
  preferredGeneratedValue,
4240
4519
  entryOverrides: target ? { targetPageObjectModelClass: target } : {},
4241
4520
  semanticNameHint: semanticNameHint2,
4521
+ semanticNameHintAlternates: alternates,
4242
4522
  pomMergeKey
4243
4523
  });
4244
4524
  return;
@@ -4253,7 +4533,6 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4253
4533
  });
4254
4534
  return;
4255
4535
  }
4256
- const innerText = getInnerText(element) || null;
4257
4536
  const clickDirective = tryGetClickDirective(element);
4258
4537
  if (clickDirective) {
4259
4538
  const clickSuffix = getComposedClickHandlerContent(element, context, innerText, clickDirective, {
@@ -4261,7 +4540,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4261
4540
  });
4262
4541
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
4263
4542
  const idOrName = getIdOrName(element) || void 0;
4264
- const semanticNameHint2 = clickHint || idOrName || conditionalHint || void 0;
4543
+ const semanticNameHint2 = clickHint || idOrName || innerText || conditionalHint || void 0;
4265
4544
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
4266
4545
  const testId = getClickDataTestId(clickSuffix);
4267
4546
  applyResolvedDataTestIdForElement({
@@ -4275,15 +4554,16 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
4275
4554
  return;
4276
4555
  }
4277
4556
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
4278
- if (existingElementDataTestId?.value) {
4557
+ if (existingElementDataTestId) {
4279
4558
  const inferredRole = getNativeRoleFromTagSuffix().toLowerCase();
4280
- const isRecognizedInteractiveRole = inferredRole === "button" || inferredRole === "input" || inferredRole === "select" || inferredRole === "vselect" || inferredRole === "checkbox" || inferredRole === "toggle" || inferredRole === "radio";
4559
+ const isRecognizedInteractiveRole = inferredRole === "button" || inferredRole === "input" || inferredRole === "select" || inferredRole === "vselect" || inferredRole === "checkbox" || inferredRole === "toggle" || inferredRole === "radio" || inferredRole === "grid" || isComponentLikeTag(element.tag);
4281
4560
  if (!isRecognizedInteractiveRole) {
4282
4561
  return;
4283
4562
  }
4284
- const identifierHint = getIdOrName(element) || conditionalHint || void 0;
4563
+ const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
4564
+ const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
4285
4565
  applyResolvedDataTestIdForElement({
4286
- preferredGeneratedValue: staticAttributeValue(existingElementDataTestId.value),
4566
+ preferredGeneratedValue,
4287
4567
  semanticNameHint: identifierHint
4288
4568
  });
4289
4569
  return;
@@ -4311,11 +4591,13 @@ function createDevProcessorPlugin(options) {
4311
4591
  nativeWrappers,
4312
4592
  excludedComponents,
4313
4593
  viewsDir,
4594
+ scanDirs,
4314
4595
  projectRootRef,
4315
4596
  normalizedBasePagePath,
4316
4597
  basePageClassPath,
4317
4598
  outDir,
4318
4599
  emitLanguages,
4600
+ csharp,
4319
4601
  generateFixtures,
4320
4602
  customPomAttachments,
4321
4603
  customPomDir,
@@ -4323,6 +4605,7 @@ function createDevProcessorPlugin(options) {
4323
4605
  testIdAttribute,
4324
4606
  routerAwarePoms,
4325
4607
  resolvedRouterEntry,
4608
+ routerType,
4326
4609
  loggerRef
4327
4610
  } = options;
4328
4611
  let scheduleVueFileRegen = null;
@@ -4336,7 +4619,11 @@ function createDevProcessorPlugin(options) {
4336
4619
  return;
4337
4620
  if (!ctx.file.endsWith(".vue"))
4338
4621
  return;
4339
- if (!ctx.file.includes(`${path.sep}src${path.sep}`))
4622
+ const isContainedInScanDirs = scanDirs.some((dir) => {
4623
+ const absDir = path.resolve(projectRootRef.current, dir);
4624
+ return ctx.file.startsWith(absDir + path.sep);
4625
+ });
4626
+ if (!isContainedInScanDirs)
4340
4627
  return;
4341
4628
  scheduleVueFileRegen(ctx.file, "hmr");
4342
4629
  },
@@ -4347,9 +4634,15 @@ function createDevProcessorPlugin(options) {
4347
4634
  setResolveToComponentNameFn(() => null);
4348
4635
  return;
4349
4636
  }
4350
- if (!resolvedRouterEntry)
4351
- throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
4352
- const { routeNameMap, routePathMap } = await parseRouterFileFromCwd(resolvedRouterEntry);
4637
+ let result;
4638
+ if (routerType === "nuxt") {
4639
+ result = await introspectNuxtPages(projectRootRef.current);
4640
+ } else {
4641
+ if (!resolvedRouterEntry)
4642
+ throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
4643
+ result = await parseRouterFileFromCwd(resolvedRouterEntry);
4644
+ }
4645
+ const { routeNameMap, routePathMap } = result;
4353
4646
  setRouteNameToComponentNameMap(routeNameMap);
4354
4647
  setResolveToComponentNameFn((to) => {
4355
4648
  if (typeof to === "string") {
@@ -4450,29 +4743,34 @@ function createDevProcessorPlugin(options) {
4450
4743
  return { componentName, ms: performance.now() - started, compiled: true };
4451
4744
  };
4452
4745
  const fullRebuildSnapshotFromFilesystem = () => {
4453
- const srcDir = path.resolve(projectRootRef.current, "src");
4454
- if (!fs.existsSync(srcDir))
4455
- return;
4456
4746
  const t0 = performance.now();
4457
4747
  snapshotHierarchy.clear();
4458
4748
  snapshotVuePathMap.clear();
4459
4749
  filePathToComponentName.clear();
4460
- const vueFiles = walkFilesRecursive(srcDir);
4461
- logInfo(`initial scan: found ${vueFiles.length} .vue files under src/`);
4750
+ let totalVueFiles = 0;
4462
4751
  let compiledCount = 0;
4463
- for (const file of vueFiles) {
4464
- const res = compileVueFileIntoSnapshot(file);
4465
- if (res.compiled)
4466
- compiledCount++;
4752
+ for (const dir of scanDirs) {
4753
+ const absDir = path.resolve(projectRootRef.current, dir);
4754
+ if (!fs.existsSync(absDir))
4755
+ continue;
4756
+ const vueFiles = walkFilesRecursive(absDir);
4757
+ totalVueFiles += vueFiles.length;
4758
+ for (const file of vueFiles) {
4759
+ const res = compileVueFileIntoSnapshot(file);
4760
+ if (res.compiled)
4761
+ compiledCount++;
4762
+ }
4467
4763
  }
4468
4764
  const t1 = performance.now();
4469
- logInfo(`initial compile: ${compiledCount}/${vueFiles.length} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
4765
+ logInfo(`initial scan: found ${totalVueFiles} .vue files in ${scanDirs.join(", ")}`);
4766
+ logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
4470
4767
  };
4471
4768
  const generateAggregatedFromSnapshot = (reason) => {
4472
4769
  const t0 = performance.now();
4473
4770
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
4474
4771
  outDir,
4475
4772
  emitLanguages,
4773
+ csharp,
4476
4774
  generateFixtures,
4477
4775
  customPomAttachments,
4478
4776
  projectRoot: projectRootRef.current,
@@ -4480,7 +4778,8 @@ function createDevProcessorPlugin(options) {
4480
4778
  customPomImportAliases,
4481
4779
  testIdAttribute,
4482
4780
  vueRouterFluentChaining: routerAwarePoms,
4483
- routerEntry: resolvedRouterEntry
4781
+ routerEntry: resolvedRouterEntry,
4782
+ routerType
4484
4783
  });
4485
4784
  const t1 = performance.now();
4486
4785
  logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
@@ -4493,9 +4792,9 @@ function createDevProcessorPlugin(options) {
4493
4792
  const t1 = performance.now();
4494
4793
  logInfo(`startup total: ${formatMs(t1 - t0)}`);
4495
4794
  })();
4496
- const watchedVueGlob = path.resolve(projectRootRef.current, "src", "**", "*.vue");
4795
+ const watchedVueGlobs = scanDirs.map((dir) => path.resolve(projectRootRef.current, dir, "**", "*.vue"));
4497
4796
  const watchedPluginGlob = path.resolve(projectRootRef.current, "vite-plugins", "vue-pom-generator", "**", "*.ts");
4498
- server.watcher.add([watchedVueGlob, watchedPluginGlob, basePageClassPath]);
4797
+ server.watcher.add([...watchedVueGlobs, watchedPluginGlob, basePageClassPath]);
4499
4798
  let timer = null;
4500
4799
  let maxWaitTimer = null;
4501
4800
  const pendingChangedVueFiles = /* @__PURE__ */ new Set();
@@ -4594,7 +4893,13 @@ function createDevProcessorPlugin(options) {
4594
4893
  server.watcher.on("add", (p) => {
4595
4894
  if (typeof p !== "string")
4596
4895
  return;
4597
- if (!p.endsWith(".vue") || !p.includes(`${path.sep}src${path.sep}`))
4896
+ if (!p.endsWith(".vue"))
4897
+ return;
4898
+ const isContainedInScanDirs = scanDirs.some((dir) => {
4899
+ const absDir = path.resolve(projectRootRef.current, dir);
4900
+ return p.startsWith(absDir + path.sep);
4901
+ });
4902
+ if (!isContainedInScanDirs)
4598
4903
  return;
4599
4904
  void (async () => {
4600
4905
  await initialBuildPromise;
@@ -4605,7 +4910,13 @@ function createDevProcessorPlugin(options) {
4605
4910
  server.watcher.on("unlink", (p) => {
4606
4911
  if (typeof p !== "string")
4607
4912
  return;
4608
- if (!p.endsWith(".vue") || !p.includes(`${path.sep}src${path.sep}`))
4913
+ if (!p.endsWith(".vue"))
4914
+ return;
4915
+ const isContainedInScanDirs = scanDirs.some((dir) => {
4916
+ const absDir = path.resolve(projectRootRef.current, dir);
4917
+ return p.startsWith(absDir + path.sep);
4918
+ });
4919
+ if (!isContainedInScanDirs)
4609
4920
  return;
4610
4921
  void (async () => {
4611
4922
  await initialBuildPromise;
@@ -4646,10 +4957,13 @@ function createSupportPlugins(options) {
4646
4957
  nativeWrappers,
4647
4958
  excludedComponents,
4648
4959
  viewsDir,
4960
+ scanDirs,
4649
4961
  outDir,
4650
4962
  emitLanguages,
4963
+ csharp,
4651
4964
  routerAwarePoms,
4652
4965
  routerEntry,
4966
+ routerType,
4653
4967
  generateFixtures,
4654
4968
  customPomAttachments,
4655
4969
  projectRootRef,
@@ -4662,6 +4976,8 @@ function createSupportPlugins(options) {
4662
4976
  const resolveRouterEntry2 = () => {
4663
4977
  if (!routerAwarePoms)
4664
4978
  return void 0;
4979
+ if (routerType === "nuxt")
4980
+ return void 0;
4665
4981
  if (!routerEntry)
4666
4982
  throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
4667
4983
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(projectRootRef.current, routerEntry);
@@ -4683,6 +4999,7 @@ function createSupportPlugins(options) {
4683
4999
  normalizedBasePagePath,
4684
5000
  outDir,
4685
5001
  emitLanguages,
5002
+ csharp,
4686
5003
  generateFixtures,
4687
5004
  customPomAttachments,
4688
5005
  projectRootRef,
@@ -4690,6 +5007,7 @@ function createSupportPlugins(options) {
4690
5007
  customPomImportAliases,
4691
5008
  testIdAttribute,
4692
5009
  routerAwarePoms,
5010
+ routerType,
4693
5011
  resolvedRouterEntry,
4694
5012
  loggerRef
4695
5013
  });
@@ -4697,6 +5015,7 @@ function createSupportPlugins(options) {
4697
5015
  nativeWrappers,
4698
5016
  excludedComponents,
4699
5017
  viewsDir,
5018
+ scanDirs,
4700
5019
  projectRootRef,
4701
5020
  normalizedBasePagePath,
4702
5021
  basePageClassPath,
@@ -4708,6 +5027,7 @@ function createSupportPlugins(options) {
4708
5027
  customPomImportAliases,
4709
5028
  testIdAttribute,
4710
5029
  routerAwarePoms,
5030
+ routerType,
4711
5031
  resolvedRouterEntry,
4712
5032
  loggerRef
4713
5033
  });
@@ -4809,7 +5129,7 @@ function tryCreateElementMetadata(args) {
4809
5129
  };
4810
5130
  return metadata;
4811
5131
  }
4812
- function extractMetadataFromAST(ast, componentName, metadataMap, semanticNameMap, testIdAttribute) {
5132
+ function extractMetadataAfterTransform(ast, componentName, elementMetadata, semanticNameMap, testIdAttribute) {
4813
5133
  const componentMetadata = /* @__PURE__ */ new Map();
4814
5134
  function traverseNode(node) {
4815
5135
  if (node.type === NodeTypes.ELEMENT) {
@@ -4848,27 +5168,8 @@ function extractMetadataFromAST(ast, componentName, metadataMap, semanticNameMap
4848
5168
  }
4849
5169
  traverseNode(ast);
4850
5170
  if (componentMetadata.size > 0) {
4851
- metadataMap.set(componentName, componentMetadata);
4852
- }
4853
- }
4854
- function compileWithMetadataExtractionManual(source, options, componentName, metadataMap, semanticNameMap, testIdAttribute = "data-testid") {
4855
- const normalizedTestIdAttribute = (testIdAttribute ?? "data-testid").trim() || "data-testid";
4856
- const result = compile(
4857
- source,
4858
- extend({}, options, {
4859
- nodeTransforms: [
4860
- ...options.nodeTransforms || [],
4861
- (node, _context) => {
4862
- if (node.type !== NodeTypes.ROOT)
4863
- return;
4864
- return () => {
4865
- extractMetadataFromAST(node, componentName, metadataMap, semanticNameMap, normalizedTestIdAttribute);
4866
- };
4867
- }
4868
- ]
4869
- })
4870
- );
4871
- return result;
5171
+ elementMetadata.set(componentName, componentMetadata);
5172
+ }
4872
5173
  }
4873
5174
  function createVuePluginWithTestIds(options) {
4874
5175
  const {
@@ -4883,27 +5184,91 @@ function createVuePluginWithTestIds(options) {
4883
5184
  excludedComponents,
4884
5185
  getViewsDirAbs,
4885
5186
  testIdAttribute,
4886
- loggerRef
5187
+ loggerRef,
5188
+ scanDirs = ["src"],
5189
+ getProjectRoot
4887
5190
  } = options;
5191
+ const getComponentNameFromPath = (filename) => {
5192
+ const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
5193
+ const projectRoot = getProjectRoot();
5194
+ const absFilename = path.isAbsolute(cleanPath) ? cleanPath : path.resolve(projectRoot, cleanPath);
5195
+ const viewsDirAbs = getViewsDirAbs();
5196
+ const roots = [viewsDirAbs, ...scanDirs.map((d) => path.resolve(projectRoot, d))];
5197
+ for (const dir of scanDirs) {
5198
+ const absDir = path.resolve(projectRoot, dir);
5199
+ try {
5200
+ const pagesDir = path.join(absDir, "pages");
5201
+ if (fs.existsSync(pagesDir)) {
5202
+ roots.push(pagesDir);
5203
+ }
5204
+ const componentsDir = path.join(absDir, "components");
5205
+ if (fs.existsSync(componentsDir)) {
5206
+ roots.push(componentsDir);
5207
+ }
5208
+ } catch {
5209
+ }
5210
+ }
5211
+ const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
5212
+ let componentName = "";
5213
+ for (const root of potentialRoots) {
5214
+ if (absFilename.startsWith(root + path.sep) || absFilename === root) {
5215
+ const rel = path.relative(root, absFilename);
5216
+ const parsed = path.parse(rel);
5217
+ const segments = path.join(parsed.dir, parsed.name);
5218
+ componentName = toPascalCase(segments);
5219
+ break;
5220
+ }
5221
+ }
5222
+ if (!componentName) {
5223
+ const parsed = path.parse(absFilename);
5224
+ componentName = toPascalCase(parsed.name);
5225
+ }
5226
+ return componentName;
5227
+ };
5228
+ const isFileInScope = (filename) => {
5229
+ if (!filename)
5230
+ return false;
5231
+ const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
5232
+ const projectRoot = getProjectRoot();
5233
+ const absFilename = path.isAbsolute(cleanPath) ? cleanPath : path.resolve(projectRoot, cleanPath);
5234
+ if (absFilename.includes(`${path.sep}node_modules${path.sep}`) || absFilename.includes("/node_modules/"))
5235
+ return false;
5236
+ const viewsDirAbs = getViewsDirAbs();
5237
+ if (absFilename.startsWith(viewsDirAbs + path.sep) || absFilename === viewsDirAbs)
5238
+ return true;
5239
+ const rootsToTry = [projectRoot, process.cwd()];
5240
+ const matched = scanDirs.some((dir) => {
5241
+ return rootsToTry.some((root) => {
5242
+ const absDir = path.resolve(root, dir);
5243
+ if (absFilename.startsWith(absDir + path.sep) || absFilename === absDir)
5244
+ return true;
5245
+ if (dir.startsWith("app/") && root.endsWith("/app")) {
5246
+ const relativeDir = dir.substring(4);
5247
+ const absDirAlt = path.resolve(root, relativeDir);
5248
+ return absFilename.startsWith(absDirAlt + path.sep) || absFilename === absDirAlt;
5249
+ }
5250
+ return false;
5251
+ });
5252
+ });
5253
+ if (cleanPath.endsWith(".vue") && !matched) {
5254
+ loggerRef.current.debug(`[isFileInScope] REJECTED: ${absFilename} (Clean: ${cleanPath})`);
5255
+ }
5256
+ return matched;
5257
+ };
4888
5258
  const userTemplate = vueOptions?.template ?? {};
4889
5259
  const userCompilerOptions = userTemplate.compilerOptions ?? {};
4890
5260
  const userNodeTransforms = userCompilerOptions.nodeTransforms ?? [];
4891
5261
  const perFileTransform = /* @__PURE__ */ new Map();
4892
- const templateCompilerOptions = {
4893
- ...userCompilerOptions,
4894
- // Ensures compiler-core runs `transformExpression` (in non-browser builds),
4895
- // which parses directive expressions via @babel/parser and attaches `exp.ast`.
4896
- // This improves reliability for AST-based consumers (like our data-testid generator).
4897
- prefixIdentifiers: true,
4898
- nodeTransforms: [
5262
+ const getNodeTransforms = (filename, componentNameOverride) => {
5263
+ const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
5264
+ const viewsDirAbs = getViewsDirAbs();
5265
+ const componentName = componentNameOverride || getComponentNameFromPath(cleanPath);
5266
+ return [
4899
5267
  ...userNodeTransforms,
4900
5268
  (node, context) => {
4901
- if (!context.filename)
4902
- return;
4903
- const componentName = path.basename(context.filename, ".vue");
4904
5269
  if (node.type === NodeTypes.ROOT) {
4905
5270
  componentHierarchyMap.delete(componentName);
4906
- vueFilesPathMap.set(componentName, context.filename);
5271
+ vueFilesPathMap.set(componentName, filename);
4907
5272
  perFileTransform.set(
4908
5273
  componentName,
4909
5274
  createTestIdTransform(
@@ -4911,113 +5276,99 @@ function createVuePluginWithTestIds(options) {
4911
5276
  componentHierarchyMap,
4912
5277
  nativeWrappers,
4913
5278
  excludedComponents,
4914
- getViewsDirAbs(),
5279
+ viewsDirAbs,
4915
5280
  {
4916
5281
  existingIdBehavior,
4917
5282
  testIdAttribute,
4918
5283
  nameCollisionBehavior,
4919
- warn: (message) => loggerRef.current.warn(message)
5284
+ warn: (message) => loggerRef.current.warn(message),
5285
+ vueFilesPathMap
4920
5286
  }
4921
5287
  )
4922
5288
  );
5289
+ return () => {
5290
+ extractMetadataAfterTransform(
5291
+ node,
5292
+ componentName,
5293
+ elementMetadata,
5294
+ semanticNameMap,
5295
+ testIdAttribute
5296
+ );
5297
+ };
4923
5298
  }
4924
5299
  let transform = perFileTransform.get(componentName);
4925
5300
  if (!transform) {
4926
5301
  componentHierarchyMap.delete(componentName);
4927
- vueFilesPathMap.set(componentName, context.filename);
5302
+ vueFilesPathMap.set(componentName, filename);
4928
5303
  transform = createTestIdTransform(
4929
5304
  componentName,
4930
5305
  componentHierarchyMap,
4931
5306
  nativeWrappers,
4932
5307
  excludedComponents,
4933
- getViewsDirAbs(),
5308
+ viewsDirAbs,
4934
5309
  {
4935
5310
  existingIdBehavior,
4936
5311
  testIdAttribute,
4937
5312
  nameCollisionBehavior,
4938
- warn: (message) => loggerRef.current.warn(message)
5313
+ warn: (message) => loggerRef.current.warn(message),
5314
+ vueFilesPathMap
4939
5315
  }
4940
5316
  );
4941
5317
  perFileTransform.set(componentName, transform);
4942
5318
  }
4943
5319
  return transform(node, context);
4944
5320
  }
5321
+ ];
5322
+ };
5323
+ const templateCompilerOptions = {
5324
+ ...userCompilerOptions,
5325
+ prefixIdentifiers: true,
5326
+ nodeTransforms: [
5327
+ // This will be used if the user uses the returned vue plugin.
5328
+ // We'll populate it via the hook below for better compatibility.
4945
5329
  ]
4946
5330
  };
5331
+ const metadataCollectorPlugin = {
5332
+ name: "vue-pom-generator-metadata-collector",
5333
+ enforce: "pre",
5334
+ async transform(code, id) {
5335
+ const cleanPath = id.includes("?") ? id.substring(0, id.indexOf("?")) : id;
5336
+ if (!cleanPath.endsWith(".vue") || !isFileInScope(id)) {
5337
+ return null;
5338
+ }
5339
+ if (id !== cleanPath) {
5340
+ return null;
5341
+ }
5342
+ const componentName = getComponentNameFromPath(cleanPath);
5343
+ loggerRef.current.debug(`Collecting metadata for ${cleanPath} (component: ${componentName})`);
5344
+ try {
5345
+ const { parse: parse2 } = await import("@vue/compiler-sfc");
5346
+ const compilerDom2 = await import("@vue/compiler-dom");
5347
+ const compile = compilerDom2.compile;
5348
+ const { descriptor } = parse2(code, { filename: cleanPath });
5349
+ if (descriptor.template) {
5350
+ compile(descriptor.template.content, {
5351
+ ...userCompilerOptions,
5352
+ filename: cleanPath,
5353
+ nodeTransforms: getNodeTransforms(cleanPath, componentName)
5354
+ });
5355
+ loggerRef.current.debug(`Metadata collected for ${cleanPath}`);
5356
+ }
5357
+ } catch (e) {
5358
+ loggerRef.current.warn(`Metadata collection failed for ${cleanPath}: ${e}`);
5359
+ }
5360
+ return null;
5361
+ }
5362
+ };
4947
5363
  const template = {
4948
5364
  ...userTemplate,
4949
- compiler: {
4950
- // Preserve the full compiler-dom module behavior (directiveTransforms, nodeTransforms, etc.).
4951
- // We only override `compile` to run our metadata extraction after transforms.
4952
- ...compilerDom,
4953
- compile(source, compilerOptions) {
4954
- const componentName = compilerOptions.filename ? path.basename(compilerOptions.filename, ".vue") : "Unknown";
4955
- return compileWithMetadataExtractionManual(
4956
- source,
4957
- compilerOptions,
4958
- componentName,
4959
- elementMetadata,
4960
- semanticNameMap,
4961
- testIdAttribute
4962
- );
4963
- }
4964
- },
4965
5365
  compilerOptions: templateCompilerOptions
4966
5366
  };
4967
- return vue({
5367
+ const internalVuePlugin = vue({
4968
5368
  ...vueOptions,
4969
5369
  template
4970
5370
  });
4971
- }
4972
- const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
4973
- function normalize(message) {
4974
- const trimmed = (message ?? "").trim();
4975
- if (!trimmed)
4976
- return VUE_POM_GENERATOR_LOG_PREFIX;
4977
- if (trimmed.startsWith(VUE_POM_GENERATOR_LOG_PREFIX))
4978
- return trimmed;
4979
- return `${VUE_POM_GENERATOR_LOG_PREFIX} ${trimmed}`;
4980
- }
4981
- function createLogger(options) {
4982
- const { verbosity, viteLogger } = options;
4983
- const sinkInfo = (msg) => {
4984
- if (viteLogger) {
4985
- viteLogger.info(normalize(msg));
4986
- return;
4987
- }
4988
- console.log(normalize(msg));
4989
- };
4990
- const sinkWarn = (msg) => {
4991
- if (viteLogger) {
4992
- viteLogger.warn(normalize(msg));
4993
- return;
4994
- }
4995
- console.warn(normalize(msg));
4996
- };
4997
- const sinkDebug = (msg) => {
4998
- if (viteLogger) {
4999
- viteLogger.info(normalize(msg));
5000
- return;
5001
- }
5002
- console.log(normalize(msg));
5003
- };
5004
- return {
5005
- info(message) {
5006
- if (verbosity === "silent")
5007
- return;
5008
- sinkInfo(message);
5009
- },
5010
- debug(message) {
5011
- if (verbosity !== "debug")
5012
- return;
5013
- sinkDebug(message);
5014
- },
5015
- warn(message) {
5016
- if (verbosity === "silent")
5017
- return;
5018
- sinkWarn(message);
5019
- }
5020
- };
5371
+ return { metadataCollectorPlugin, internalVuePlugin };
5021
5372
  }
5022
5373
  function assertNonEmptyString(value, name) {
5023
5374
  if (!value || !value.trim()) {
@@ -5032,9 +5383,10 @@ function createVuePomGeneratorPlugins(options = {}) {
5032
5383
  const generationSetting = options.generation;
5033
5384
  const generationOptions = generationSetting === false ? null : generationSetting ?? {};
5034
5385
  const generationEnabled = generationOptions !== null;
5035
- const verbosity = options.logging?.verbosity ?? "info";
5386
+ const verbosity = options.logging?.verbosity ?? "warn";
5036
5387
  const vueOptions = options.vueOptions;
5037
5388
  const viewsDir = injection.viewsDir ?? "src/views";
5389
+ const scanDirs = injection.scanDirs ?? ["src"];
5038
5390
  const nativeWrappers = injection.nativeWrappers ?? {};
5039
5391
  const excludedComponents = injection.excludeComponents ?? [];
5040
5392
  const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
@@ -5043,6 +5395,8 @@ function createVuePomGeneratorPlugins(options = {}) {
5043
5395
  const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
5044
5396
  const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
5045
5397
  const routerEntry = generationOptions?.router?.entry;
5398
+ const routerType = generationOptions?.router?.type ?? "vue-router";
5399
+ const csharp = generationOptions?.csharp;
5046
5400
  const generateFixtures = generationOptions?.playwright?.fixtures;
5047
5401
  const customPoms = generationOptions?.playwright?.customPoms;
5048
5402
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
@@ -5063,11 +5417,12 @@ function createVuePomGeneratorPlugins(options = {}) {
5063
5417
  assertNonEmptyString(viewsDir, "[vue-pom-generator] injection.viewsDir");
5064
5418
  if (generationEnabled) {
5065
5419
  assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
5066
- if (generationOptions?.router) {
5420
+ if (generationOptions?.router && routerType === "vue-router") {
5067
5421
  assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
5068
5422
  }
5069
5423
  }
5070
- loggerRef.current.debug(`projectRoot=${projectRootRef.current}`);
5424
+ loggerRef.current.info(`projectRoot=${projectRootRef.current}`);
5425
+ loggerRef.current.info(`Active plugins: ${config.plugins.map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
5071
5426
  }
5072
5427
  };
5073
5428
  const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, viewsDir);
@@ -5076,7 +5431,7 @@ function createVuePomGeneratorPlugins(options = {}) {
5076
5431
  const semanticNameMap = /* @__PURE__ */ new Map();
5077
5432
  const componentHierarchyMap = /* @__PURE__ */ new Map();
5078
5433
  const vueFilesPathMap = /* @__PURE__ */ new Map();
5079
- const vuePlugin = createVuePluginWithTestIds({
5434
+ const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
5080
5435
  vueOptions,
5081
5436
  existingIdBehavior,
5082
5437
  nameCollisionBehavior,
@@ -5088,13 +5443,11 @@ function createVuePomGeneratorPlugins(options = {}) {
5088
5443
  excludedComponents,
5089
5444
  getViewsDirAbs,
5090
5445
  testIdAttribute,
5091
- loggerRef
5446
+ loggerRef,
5447
+ scanDirs,
5448
+ getProjectRoot: () => projectRootRef.current
5092
5449
  });
5093
- if (!generationEnabled) {
5094
- const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
5095
- return [configPlugin, vuePlugin, virtualModules];
5096
- }
5097
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0;
5450
+ const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
5098
5451
  const supportPlugins = createSupportPlugins({
5099
5452
  componentTestIds,
5100
5453
  componentHierarchyMap,
@@ -5102,8 +5455,10 @@ function createVuePomGeneratorPlugins(options = {}) {
5102
5455
  nativeWrappers,
5103
5456
  excludedComponents,
5104
5457
  viewsDir,
5458
+ scanDirs,
5105
5459
  outDir,
5106
5460
  emitLanguages,
5461
+ csharp,
5107
5462
  routerAwarePoms,
5108
5463
  routerEntry,
5109
5464
  generateFixtures,
@@ -5113,9 +5468,29 @@ function createVuePomGeneratorPlugins(options = {}) {
5113
5468
  customPomDir: resolvedCustomPomDir,
5114
5469
  customPomImportAliases: resolvedCustomPomImportAliases,
5115
5470
  testIdAttribute,
5116
- loggerRef
5471
+ loggerRef,
5472
+ routerType
5117
5473
  });
5118
- return [configPlugin, vuePlugin, ...supportPlugins];
5474
+ const isNuxt = routerType === "nuxt";
5475
+ if (isNuxt) {
5476
+ loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");
5477
+ }
5478
+ const resultPlugins = [
5479
+ configPlugin,
5480
+ metadataCollectorPlugin,
5481
+ ...isNuxt ? [] : [internalVuePlugin],
5482
+ ...supportPlugins
5483
+ ];
5484
+ if (!generationEnabled) {
5485
+ const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
5486
+ return [
5487
+ configPlugin,
5488
+ metadataCollectorPlugin,
5489
+ ...isNuxt ? [] : [internalVuePlugin],
5490
+ virtualModules
5491
+ ];
5492
+ }
5493
+ return resultPlugins;
5119
5494
  }
5120
5495
  export {
5121
5496
  createVuePomGeneratorPlugins,