@immense/vue-pom-generator 1.0.32 → 1.0.34

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
@@ -2528,6 +2528,53 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2528
2528
  registerGeneratedMethodSignature(generatedName, signature);
2529
2529
  }
2530
2530
  }
2531
+ function safeRealpath(value) {
2532
+ try {
2533
+ if (fs.existsSync(value)) {
2534
+ return fs.realpathSync(value);
2535
+ }
2536
+ } catch {
2537
+ return value;
2538
+ }
2539
+ const parent = path.dirname(value);
2540
+ if (!parent || parent === value) {
2541
+ return value;
2542
+ }
2543
+ const resolvedParent = safeRealpath(parent);
2544
+ return resolvedParent === parent ? value : path.join(resolvedParent, path.basename(value));
2545
+ }
2546
+ function resolveComponentNameFromPath(options) {
2547
+ const { projectRoot, viewsDirAbs, scanDirs, extraRoots = [] } = options;
2548
+ const cleanFilename = options.filename.includes("?") ? options.filename.substring(0, options.filename.indexOf("?")) : options.filename;
2549
+ const absFilename = path.isAbsolute(cleanFilename) ? cleanFilename : path.resolve(projectRoot, cleanFilename);
2550
+ const normalizedAbsFilename = path.normalize(safeRealpath(absFilename));
2551
+ const rootBases = [projectRoot, ...extraRoots.filter((r) => r !== projectRoot)];
2552
+ const roots = [viewsDirAbs, ...rootBases.flatMap((base) => scanDirs.map((d) => path.resolve(base, d)))];
2553
+ for (const base of rootBases) {
2554
+ for (const dir of scanDirs) {
2555
+ const absDir = path.resolve(base, dir);
2556
+ try {
2557
+ const pagesDir = path.join(absDir, "pages");
2558
+ if (fs.existsSync(pagesDir))
2559
+ roots.push(pagesDir);
2560
+ const componentsDir = path.join(absDir, "components");
2561
+ if (fs.existsSync(componentsDir))
2562
+ roots.push(componentsDir);
2563
+ } catch {
2564
+ }
2565
+ }
2566
+ }
2567
+ const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(safeRealpath(r))))).sort((a, b) => b.length - a.length);
2568
+ for (const root of potentialRoots) {
2569
+ if (normalizedAbsFilename.startsWith(root + path.sep) || normalizedAbsFilename === root) {
2570
+ const rel = path.relative(root, normalizedAbsFilename);
2571
+ const parsed = path.parse(rel);
2572
+ const segments = path.join(parsed.dir, parsed.name);
2573
+ return toPascalCase(segments);
2574
+ }
2575
+ }
2576
+ return toPascalCase(path.parse(normalizedAbsFilename).name);
2577
+ }
2531
2578
  let routerIntrospectionQueue = Promise.resolve();
2532
2579
  async function runRouterIntrospectionExclusive(fn) {
2533
2580
  const prev = routerIntrospectionQueue.catch(() => void 0);
@@ -2807,20 +2854,84 @@ function getRouteParamMeta(router, record, paramNames) {
2807
2854
  }
2808
2855
  });
2809
2856
  }
2810
- function getComponentNameFromRouteRecord(record) {
2811
- const comp = record.components?.default;
2812
- if (!comp)
2857
+ function normalizeRouteComponentFilePath(filePath, options = {}) {
2858
+ const queryIndex = filePath.indexOf("?");
2859
+ const cleanPath = queryIndex === -1 ? filePath : filePath.slice(0, queryIndex);
2860
+ if (cleanPath.startsWith("/@fs/")) {
2861
+ return path.normalize(cleanPath.slice("/@fs/".length));
2862
+ }
2863
+ if (path.isAbsolute(cleanPath)) {
2864
+ if (fs.existsSync(cleanPath) || !options.rootDir)
2865
+ return path.normalize(cleanPath);
2866
+ return path.normalize(path.resolve(options.rootDir, `.${cleanPath}`));
2867
+ }
2868
+ if (!options.rootDir)
2813
2869
  return null;
2870
+ return path.normalize(path.resolve(options.rootDir, cleanPath));
2871
+ }
2872
+ function getComponentInfoFromVueComponent(comp, options = {}) {
2873
+ if (!comp) {
2874
+ return {
2875
+ componentName: null,
2876
+ filePath: null
2877
+ };
2878
+ }
2879
+ let componentName = null;
2880
+ let filePath = null;
2814
2881
  if (typeof comp.__file === "string" && comp.__file.length) {
2882
+ filePath = normalizeRouteComponentFilePath(comp.__file, { rootDir: options.rootDir });
2815
2883
  const base = path.posix.basename(path.posix.normalize(comp.__file));
2816
2884
  if (base.toLowerCase().endsWith(".vue"))
2817
- return base.slice(0, -".vue".length);
2885
+ componentName = base.slice(0, -".vue".length);
2818
2886
  }
2819
- if (typeof comp.__name === "string" && comp.__name.length)
2820
- return comp.__name;
2821
- if (typeof comp.name === "string" && comp.name.length)
2822
- return comp.name;
2823
- return null;
2887
+ if (!componentName && typeof comp.__name === "string" && comp.__name.length)
2888
+ componentName = comp.__name;
2889
+ if (!componentName && options.allowFunctionNameFallback !== false && typeof comp.name === "string" && comp.name.length) {
2890
+ componentName = comp.name;
2891
+ }
2892
+ return {
2893
+ componentName,
2894
+ filePath
2895
+ };
2896
+ }
2897
+ async function getComponentInfoFromRouteRecord(record, options = {}) {
2898
+ const comp = record.components?.default;
2899
+ if (!comp) {
2900
+ return {
2901
+ componentName: null,
2902
+ filePath: null
2903
+ };
2904
+ }
2905
+ if (typeof comp !== "function") {
2906
+ return getComponentInfoFromVueComponent(comp, options);
2907
+ }
2908
+ const directInfo = getComponentInfoFromVueComponent(comp, {
2909
+ allowFunctionNameFallback: false,
2910
+ rootDir: options.rootDir
2911
+ });
2912
+ if (directInfo.componentName || directInfo.filePath)
2913
+ return directInfo;
2914
+ try {
2915
+ const loaded = await comp();
2916
+ const resolved = loaded && typeof loaded === "object" && "default" in loaded ? loaded.default : loaded;
2917
+ const loadedInfo = getComponentInfoFromVueComponent(resolved, options);
2918
+ if (loadedInfo.componentName || loadedInfo.filePath)
2919
+ return loadedInfo;
2920
+ } catch {
2921
+ }
2922
+ return getComponentInfoFromVueComponent(comp, options);
2923
+ }
2924
+ function resolveIntrospectedComponentName(componentInfo, componentNaming) {
2925
+ if (componentInfo.filePath && componentNaming) {
2926
+ return resolveComponentNameFromPath({
2927
+ filename: componentInfo.filePath,
2928
+ projectRoot: componentNaming.projectRoot,
2929
+ viewsDirAbs: componentNaming.viewsDirAbs,
2930
+ scanDirs: componentNaming.scanDirs,
2931
+ extraRoots: componentNaming.extraRoots
2932
+ });
2933
+ }
2934
+ return componentInfo.componentName;
2824
2935
  }
2825
2936
  async function ensureDomShim() {
2826
2937
  const domShimHtml = "<!doctype html><html><head></head><body><div id='app'></div></body></html>";
@@ -3066,7 +3177,8 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
3066
3177
  const routePathMap = /* @__PURE__ */ new Map();
3067
3178
  const routeMetaEntries = [];
3068
3179
  for (const r of router.getRoutes()) {
3069
- const componentName = getComponentNameFromRouteRecord(r);
3180
+ const componentInfo = await getComponentInfoFromRouteRecord(r, { rootDir: cwd });
3181
+ const componentName = resolveIntrospectedComponentName(componentInfo, options.componentNaming);
3070
3182
  if (!componentName)
3071
3183
  continue;
3072
3184
  if (typeof r.path === "string" && r.path.length) {
@@ -3124,9 +3236,20 @@ function resolveRouterEntry(projectRoot, routerEntry) {
3124
3236
  const root = projectRoot ?? process.cwd();
3125
3237
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
3126
3238
  }
3127
- async function getRouteMetaByComponent(projectRoot, routerEntry, routerType) {
3239
+ async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, options = {}) {
3128
3240
  const root = projectRoot ?? process.cwd();
3129
- const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry));
3241
+ const viewsDir = options.viewsDir ?? "src/views";
3242
+ const viewsDirAbs = path.isAbsolute(viewsDir) ? viewsDir : path.resolve(root, viewsDir);
3243
+ const scanDirs = options.scanDirs?.length ? options.scanDirs : ["src"];
3244
+ const extraRoots = process.cwd() !== root ? [process.cwd()] : [];
3245
+ const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry), {
3246
+ componentNaming: {
3247
+ projectRoot: root,
3248
+ viewsDirAbs,
3249
+ scanDirs,
3250
+ extraRoots
3251
+ }
3252
+ });
3130
3253
  const map = /* @__PURE__ */ new Map();
3131
3254
  for (const entry of routeMetaEntries) {
3132
3255
  const list = map.get(entry.componentName) ?? [];
@@ -3303,11 +3426,17 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3303
3426
  csharp,
3304
3427
  vueRouterFluentChaining,
3305
3428
  routerEntry,
3306
- routerType
3429
+ routerType,
3430
+ viewsDir,
3431
+ scanDirs,
3432
+ routeMetaByComponent: routeMetaByComponentOverride
3307
3433
  } = options;
3308
3434
  const emitLanguages = emitLanguagesOverride?.length ? emitLanguagesOverride : ["ts"];
3309
3435
  const outDir = outDirOverride ?? "./pom";
3310
- const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType) : void 0;
3436
+ const routeMetaByComponent = routeMetaByComponentOverride ?? (vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType, {
3437
+ viewsDir,
3438
+ scanDirs
3439
+ }) : void 0);
3311
3440
  const generatedFilePaths = [];
3312
3441
  const writeGeneratedFile = (file) => {
3313
3442
  const resolvedFilePath = path.resolve(file.filePath);
@@ -4407,10 +4536,38 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
4407
4536
  return `${content}
4408
4537
  `;
4409
4538
  }
4539
+ function getGenerationMetrics(componentHierarchyMap) {
4540
+ let selectorCount = 0;
4541
+ let generatedMethodCount = 0;
4542
+ for (const deps of componentHierarchyMap.values()) {
4543
+ selectorCount += deps.dataTestIdSet?.size ?? 0;
4544
+ generatedMethodCount += deps.generatedMethods?.size ?? 0;
4545
+ }
4546
+ return {
4547
+ entryCount: componentHierarchyMap.size,
4548
+ selectorCount,
4549
+ generatedMethodCount
4550
+ };
4551
+ }
4552
+ function isLessRich(current, previous) {
4553
+ if (current.entryCount !== previous.entryCount) {
4554
+ return current.entryCount < previous.entryCount;
4555
+ }
4556
+ if (current.selectorCount !== previous.selectorCount) {
4557
+ return current.selectorCount < previous.selectorCount;
4558
+ }
4559
+ return current.generatedMethodCount < previous.generatedMethodCount;
4560
+ }
4561
+ function getGenerationMetricsKey(projectRoot, outDir) {
4562
+ return path.resolve(projectRoot, outDir ?? "./pom");
4563
+ }
4564
+ const buildGenerationMetricsByOutputKey = /* @__PURE__ */ new Map();
4410
4565
  function createBuildProcessorPlugin(options) {
4411
4566
  const {
4412
4567
  componentHierarchyMap,
4413
4568
  vueFilesPathMap,
4569
+ viewsDir,
4570
+ scanDirs,
4414
4571
  basePageClassPath,
4415
4572
  normalizedBasePagePath,
4416
4573
  outDir,
@@ -4429,7 +4586,6 @@ function createBuildProcessorPlugin(options) {
4429
4586
  routerModuleShims,
4430
4587
  loggerRef
4431
4588
  } = options;
4432
- let lastGeneratedEntryCount = 0;
4433
4589
  return {
4434
4590
  name: "vue-pom-generator-build",
4435
4591
  // This plugin exists to generate code on build output; it is not needed during dev-server HMR.
@@ -4447,7 +4603,14 @@ function createBuildProcessorPlugin(options) {
4447
4603
  } else {
4448
4604
  if (!resolvedRouterEntry)
4449
4605
  throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
4450
- result = await parseRouterFileFromCwd(resolvedRouterEntry, { moduleShims: routerModuleShims });
4606
+ result = await parseRouterFileFromCwd(resolvedRouterEntry, {
4607
+ moduleShims: routerModuleShims,
4608
+ componentNaming: {
4609
+ projectRoot: projectRootRef.current,
4610
+ viewsDirAbs: path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir),
4611
+ scanDirs
4612
+ }
4613
+ });
4451
4614
  }
4452
4615
  const { routeNameMap, routePathMap } = result;
4453
4616
  setRouteNameToComponentNameMap(routeNameMap);
@@ -4476,11 +4639,13 @@ function createBuildProcessorPlugin(options) {
4476
4639
  this.addWatchFile(pointerPath);
4477
4640
  },
4478
4641
  buildEnd() {
4479
- const entryCount = componentHierarchyMap.size;
4480
- if (entryCount <= 0) {
4642
+ const metrics = getGenerationMetrics(componentHierarchyMap);
4643
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
4481
4644
  return;
4482
4645
  }
4483
- if (entryCount < lastGeneratedEntryCount) {
4646
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
4647
+ const previousMetrics = buildGenerationMetricsByOutputKey.get(generationMetricsKey);
4648
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
4484
4649
  return;
4485
4650
  }
4486
4651
  generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
@@ -4496,10 +4661,12 @@ function createBuildProcessorPlugin(options) {
4496
4661
  testIdAttribute,
4497
4662
  vueRouterFluentChaining: routerAwarePoms,
4498
4663
  routerEntry: resolvedRouterEntry,
4499
- routerType
4664
+ routerType,
4665
+ viewsDir,
4666
+ scanDirs
4500
4667
  });
4501
- lastGeneratedEntryCount = entryCount;
4502
- loggerRef.current.info(`generated POMs (${entryCount} entries)`);
4668
+ buildGenerationMetricsByOutputKey.set(generationMetricsKey, metrics);
4669
+ loggerRef.current.info(`generated POMs (${metrics.entryCount} entries, ${metrics.selectorCount} selectors)`);
4503
4670
  },
4504
4671
  closeBundle() {
4505
4672
  loggerRef.current.info("build complete");
@@ -4537,6 +4704,89 @@ function toKebabCaseTag(tag) {
4537
4704
  }
4538
4705
  return result;
4539
4706
  }
4707
+ function getStaticAttributeContent(element, name) {
4708
+ const attr = element.props.find((prop) => {
4709
+ return prop.type === NodeTypes.ATTRIBUTE && prop.name === name;
4710
+ });
4711
+ return attr?.value?.content?.trim() || null;
4712
+ }
4713
+ function getNativeHtmlControlRole(element) {
4714
+ const tag = (element.tag || "").toLowerCase();
4715
+ const type = (getStaticAttributeContent(element, "type") || "").toLowerCase();
4716
+ if (tag === "textarea") {
4717
+ return "input";
4718
+ }
4719
+ if (tag === "select") {
4720
+ return "select";
4721
+ }
4722
+ if (tag !== "input") {
4723
+ return null;
4724
+ }
4725
+ if (type === "radio") {
4726
+ return "radio";
4727
+ }
4728
+ if (type === "checkbox") {
4729
+ return "checkbox";
4730
+ }
4731
+ return "input";
4732
+ }
4733
+ function normalizeControlLabelText(value) {
4734
+ const normalized = (value ?? "").replace(/\*/g, " ").replace(/\s+/g, " ").trim();
4735
+ return normalized || null;
4736
+ }
4737
+ function getLabelNodeText(labelNode) {
4738
+ for (const child of labelNode.children || []) {
4739
+ if (child.type === NodeTypes.TEXT) {
4740
+ const normalized2 = normalizeControlLabelText(child.content);
4741
+ if (normalized2) {
4742
+ return normalized2;
4743
+ }
4744
+ continue;
4745
+ }
4746
+ if (child.type !== NodeTypes.ELEMENT) {
4747
+ continue;
4748
+ }
4749
+ if (getNativeHtmlControlRole(child)) {
4750
+ continue;
4751
+ }
4752
+ const normalized = normalizeControlLabelText(getInnerText(child));
4753
+ if (normalized) {
4754
+ return normalized;
4755
+ }
4756
+ }
4757
+ return normalizeControlLabelText(getInnerText(labelNode));
4758
+ }
4759
+ function getAssociatedLabelText(element, hierarchyMap2) {
4760
+ let parent = hierarchyMap2.get(element) || null;
4761
+ while (parent) {
4762
+ if (parent.tag === "label") {
4763
+ return getLabelNodeText(parent);
4764
+ }
4765
+ parent = hierarchyMap2.get(parent) || null;
4766
+ }
4767
+ const id = getStaticAttributeContent(element, "id");
4768
+ if (!id) {
4769
+ return null;
4770
+ }
4771
+ const candidates = /* @__PURE__ */ new Set();
4772
+ for (const child of hierarchyMap2.keys()) {
4773
+ candidates.add(child);
4774
+ }
4775
+ for (const maybeParent of hierarchyMap2.values()) {
4776
+ if (maybeParent) {
4777
+ candidates.add(maybeParent);
4778
+ }
4779
+ }
4780
+ for (const candidate of candidates) {
4781
+ if (candidate.tag !== "label") {
4782
+ continue;
4783
+ }
4784
+ if (getStaticAttributeContent(candidate, "for") === id) {
4785
+ return getLabelNodeText(candidate);
4786
+ }
4787
+ }
4788
+ return null;
4789
+ }
4540
4790
  function normalizeSearchRoots(wrapperSearchRoots) {
4541
4791
  const normalized = /* @__PURE__ */ new Set();
4542
4792
  for (const root of wrapperSearchRoots) {
@@ -5069,14 +5319,14 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5069
5319
  const warn = options.warn;
5070
5320
  const vueFilesPathMap = options.vueFilesPathMap;
5071
5321
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
5072
- const safeRealpath = (p) => {
5322
+ const safeRealpath2 = (p) => {
5073
5323
  try {
5074
5324
  return fs.existsSync(p) ? fs.realpathSync(p) : p;
5075
5325
  } catch {
5076
5326
  return p;
5077
5327
  }
5078
5328
  };
5079
- const normalizedViewsDirAbs = path.normalize(safeRealpath(path.resolve(viewsDirAbs)));
5329
+ const normalizedViewsDirAbs = path.normalize(safeRealpath2(path.resolve(viewsDirAbs)));
5080
5330
  const generatedMethodContentByComponent = /* @__PURE__ */ new Map();
5081
5331
  const lastConditionalHintByParent = /* @__PURE__ */ new WeakMap();
5082
5332
  const lastConditionalMergeGroupByParent = /* @__PURE__ */ new WeakMap();
@@ -5164,7 +5414,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5164
5414
  const parentIsRoot = context?.parent?.type === NodeTypes.ROOT;
5165
5415
  const parentElement = !parentIsRoot && context?.parent?.type === NodeTypes.ELEMENT ? context.parent : null;
5166
5416
  hierarchyMap.set(element, parentElement);
5167
- const normalizeFilePath = (filePath) => path.normalize(safeRealpath(path.resolve(filePath)));
5417
+ const normalizeFilePath = (filePath) => path.normalize(safeRealpath2(path.resolve(filePath)));
5168
5418
  const normalizedFilePath = normalizeFilePath(context.filename);
5169
5419
  const parentComponentName = componentName;
5170
5420
  const dependencies = (() => {
@@ -5204,7 +5454,9 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5204
5454
  }
5205
5455
  }
5206
5456
  const getBestAvailableKeyValue = () => {
5207
- const vForKey = getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5457
+ const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
5458
+ const isDirectVForChild = parentNode?.type === NodeTypes.FOR;
5459
+ const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5208
5460
  if (vForKey) return vForKey;
5209
5461
  return getContainedInSlotDataKeyValue(element, hierarchyMap);
5210
5462
  };
@@ -5361,6 +5613,50 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5361
5613
  });
5362
5614
  return;
5363
5615
  }
5616
+ const nativeHtmlRole = getNativeHtmlControlRole(element);
5617
+ if (nativeHtmlRole) {
5618
+ const rawIdentifier = getStaticAttributeContent(element, "id") || getStaticAttributeContent(element, "name");
5619
+ const labelText = getAssociatedLabelText(element, hierarchyMap);
5620
+ const { vModel, modelValue } = getModelBindingValues(element);
5621
+ const bindingHint = modelValue || vModel || null;
5622
+ const labelToken = labelText ? toPascalCase(labelText) : "";
5623
+ const bindingToken = bindingHint ? toPascalCase(bindingHint) : "";
5624
+ let identifierToken = null;
5625
+ let semanticNameHint2;
5626
+ if (nativeHtmlRole === "radio" || nativeHtmlRole === "checkbox") {
5627
+ if (rawIdentifier) {
5628
+ identifierToken = rawIdentifier;
5629
+ semanticNameHint2 = rawIdentifier;
5630
+ } else if (bindingToken && labelToken) {
5631
+ identifierToken = `${bindingToken}${labelToken}`;
5632
+ semanticNameHint2 = `${bindingHint || bindingToken} ${labelText || labelToken}`;
5633
+ } else if (labelToken) {
5634
+ identifierToken = labelToken;
5635
+ semanticNameHint2 = labelText || labelToken;
5636
+ } else if (bindingToken) {
5637
+ identifierToken = bindingToken;
5638
+ semanticNameHint2 = bindingHint || bindingToken;
5639
+ }
5640
+ } else if (rawIdentifier) {
5641
+ identifierToken = rawIdentifier;
5642
+ semanticNameHint2 = rawIdentifier;
5643
+ } else if (labelToken) {
5644
+ identifierToken = labelToken;
5645
+ semanticNameHint2 = labelText || labelToken;
5646
+ } else if (bindingToken) {
5647
+ identifierToken = bindingToken;
5648
+ semanticNameHint2 = bindingHint || bindingToken;
5649
+ }
5650
+ if (identifierToken) {
5651
+ const preferredGeneratedValue = bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}-${identifierToken}-${nativeHtmlRole}`) : staticAttributeValue(`${componentName}-${identifierToken}-${nativeHtmlRole}`);
5652
+ applyResolvedDataTestIdForElement({
5653
+ preferredGeneratedValue,
5654
+ nativeRoleOverride: nativeHtmlRole,
5655
+ semanticNameHint: semanticNameHint2 || conditionalHint || void 0
5656
+ });
5657
+ return;
5658
+ }
5659
+ }
5364
5660
  const innerText = getInnerText(element) || null;
5365
5661
  const toDirective = nodeHasToDirective(element);
5366
5662
  if (toDirective) {
@@ -5402,12 +5698,14 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5402
5698
  });
5403
5699
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
5404
5700
  const idOrName = getIdOrName(element) || void 0;
5405
- const semanticNameHint2 = clickHint || idOrName || innerText || conditionalHint || void 0;
5701
+ const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
5702
+ const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
5406
5703
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
5407
5704
  const testId = getClickDataTestId(clickSuffix);
5408
5705
  applyResolvedDataTestIdForElement({
5409
5706
  preferredGeneratedValue: testId,
5410
5707
  semanticNameHint: semanticNameHint2,
5708
+ semanticNameHintAlternates,
5411
5709
  pomMergeKey
5412
5710
  });
5413
5711
  {
@@ -5448,37 +5746,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5448
5746
  }
5449
5747
  };
5450
5748
  }
5451
- function resolveComponentNameFromPath(options) {
5452
- const { projectRoot, viewsDirAbs, scanDirs, extraRoots = [] } = options;
5453
- const cleanFilename = options.filename.includes("?") ? options.filename.substring(0, options.filename.indexOf("?")) : options.filename;
5454
- const absFilename = path.isAbsolute(cleanFilename) ? cleanFilename : path.resolve(projectRoot, cleanFilename);
5455
- const rootBases = [projectRoot, ...extraRoots.filter((r) => r !== projectRoot)];
5456
- const roots = [viewsDirAbs, ...rootBases.flatMap((base) => scanDirs.map((d) => path.resolve(base, d)))];
5457
- for (const base of rootBases) {
5458
- for (const dir of scanDirs) {
5459
- const absDir = path.resolve(base, dir);
5460
- try {
5461
- const pagesDir = path.join(absDir, "pages");
5462
- if (fs.existsSync(pagesDir))
5463
- roots.push(pagesDir);
5464
- const componentsDir = path.join(absDir, "components");
5465
- if (fs.existsSync(componentsDir))
5466
- roots.push(componentsDir);
5467
- } catch {
5468
- }
5469
- }
5470
- }
5471
- const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
5472
- for (const root of potentialRoots) {
5473
- if (absFilename.startsWith(root + path.sep) || absFilename === root) {
5474
- const rel = path.relative(root, absFilename);
5475
- const parsed = path.parse(rel);
5476
- const segments = path.join(parsed.dir, parsed.name);
5477
- return toPascalCase(segments);
5478
- }
5479
- }
5480
- return toPascalCase(path.parse(absFilename).name);
5481
- }
5749
+ const devStartupMetricsByOutputKey = /* @__PURE__ */ new Map();
5482
5750
  function createDevProcessorPlugin(options) {
5483
5751
  const {
5484
5752
  nativeWrappers,
@@ -5524,6 +5792,7 @@ function createDevProcessorPlugin(options) {
5524
5792
  scheduleVueFileRegen(ctx.file, "hmr");
5525
5793
  },
5526
5794
  configureServer(server) {
5795
+ const getViewsDirAbs = () => path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir);
5527
5796
  const routerInitPromise = (async () => {
5528
5797
  if (!routerAwarePoms) {
5529
5798
  setRouteNameToComponentNameMap(/* @__PURE__ */ new Map());
@@ -5536,7 +5805,15 @@ function createDevProcessorPlugin(options) {
5536
5805
  } else {
5537
5806
  if (!resolvedRouterEntry)
5538
5807
  throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
5539
- result = await parseRouterFileFromCwd(resolvedRouterEntry, { moduleShims: routerModuleShims });
5808
+ result = await parseRouterFileFromCwd(resolvedRouterEntry, {
5809
+ moduleShims: routerModuleShims,
5810
+ componentNaming: {
5811
+ projectRoot: projectRootRef.current,
5812
+ viewsDirAbs: getViewsDirAbs(),
5813
+ scanDirs,
5814
+ extraRoots: [process.cwd()]
5815
+ }
5816
+ });
5540
5817
  }
5541
5818
  const { routeNameMap, routePathMap } = result;
5542
5819
  setRouteNameToComponentNameMap(routeNameMap);
@@ -5559,7 +5836,6 @@ function createDevProcessorPlugin(options) {
5559
5836
  const logDebug = (message) => loggerRef.current.debug(message);
5560
5837
  let scheduleVueFileRegenLocal = null;
5561
5838
  const formatMs = (ms) => `${ms.toFixed(1)}ms`;
5562
- const getViewsDirAbs = () => path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir);
5563
5839
  const extractTemplateFromSfc = (source, filename) => {
5564
5840
  const { descriptor } = parse$1(source, {
5565
5841
  filename: filename ?? "anonymous.vue"
@@ -5672,6 +5948,21 @@ function createDevProcessorPlugin(options) {
5672
5948
  logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
5673
5949
  };
5674
5950
  const generateAggregatedFromSnapshot = (reason) => {
5951
+ const metrics = getGenerationMetrics(snapshotHierarchy);
5952
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
5953
+ logInfo(`generate(${reason}): skipped empty snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`);
5954
+ return;
5955
+ }
5956
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
5957
+ if (reason === "startup") {
5958
+ const previousMetrics = devStartupMetricsByOutputKey.get(generationMetricsKey);
5959
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
5960
+ logInfo(
5961
+ `generate(${reason}): skipped smaller snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`
5962
+ );
5963
+ return;
5964
+ }
5965
+ }
5675
5966
  const t0 = performance.now();
5676
5967
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
5677
5968
  outDir,
@@ -5686,10 +5977,15 @@ function createDevProcessorPlugin(options) {
5686
5977
  testIdAttribute,
5687
5978
  vueRouterFluentChaining: routerAwarePoms,
5688
5979
  routerEntry: resolvedRouterEntry,
5689
- routerType
5980
+ routerType,
5981
+ viewsDir,
5982
+ scanDirs
5690
5983
  });
5984
+ if (reason === "startup") {
5985
+ devStartupMetricsByOutputKey.set(generationMetricsKey, metrics);
5986
+ }
5691
5987
  const t1 = performance.now();
5692
- logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
5988
+ logInfo(`generate(${reason}): components=${metrics.entryCount} selectors=${metrics.selectorCount} in ${formatMs(t1 - t0)}`);
5693
5989
  };
5694
5990
  const initialBuildPromise = (async () => {
5695
5991
  const t0 = performance.now();
@@ -5905,6 +6201,8 @@ function createSupportPlugins(options) {
5905
6201
  const tsProcessor = createBuildProcessorPlugin({
5906
6202
  componentHierarchyMap,
5907
6203
  vueFilesPathMap,
6204
+ viewsDir,
6205
+ scanDirs,
5908
6206
  basePageClassPath,
5909
6207
  normalizedBasePagePath,
5910
6208
  outDir,
@@ -6210,20 +6508,21 @@ function createVuePluginWithTestIds(options) {
6210
6508
  }
6211
6509
  ];
6212
6510
  };
6511
+ const runtimeNodeTransform = (node, context) => {
6512
+ const filename = context.filename;
6513
+ if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6514
+ return;
6515
+ }
6516
+ const transforms = getNodeTransforms(filename);
6517
+ const ourTransform = transforms[transforms.length - 1];
6518
+ return ourTransform(node, context);
6519
+ };
6213
6520
  const templateCompilerOptions = {
6214
6521
  ...userCompilerOptions,
6215
6522
  prefixIdentifiers: true,
6216
6523
  nodeTransforms: [
6217
6524
  ...userNodeTransforms,
6218
- (node, context) => {
6219
- const filename = context.filename;
6220
- if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6221
- return;
6222
- }
6223
- const transforms = getNodeTransforms(filename);
6224
- const ourTransform = transforms[transforms.length - 1];
6225
- return ourTransform(node, context);
6226
- }
6525
+ runtimeNodeTransform
6227
6526
  ]
6228
6527
  };
6229
6528
  const metadataCollectorPlugin = {
@@ -6262,7 +6561,42 @@ function createVuePluginWithTestIds(options) {
6262
6561
  ...vueOptions,
6263
6562
  template
6264
6563
  });
6265
- return { metadataCollectorPlugin, internalVuePlugin };
6564
+ const nuxtVueBridgePlugin = {
6565
+ name: "vue-pom-generator-nuxt-vue-bridge",
6566
+ apply: "serve",
6567
+ configResolved(config) {
6568
+ const viteVuePlugin = config.plugins.find((plugin) => {
6569
+ return typeof plugin === "object" && plugin !== null && "name" in plugin && plugin.name === "vite:vue" && "api" in plugin;
6570
+ });
6571
+ const api = viteVuePlugin?.api;
6572
+ if (!api) {
6573
+ loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
6574
+ return;
6575
+ }
6576
+ const currentOptions = api.options ?? {};
6577
+ const currentTemplate = currentOptions.template ?? {};
6578
+ const currentCompilerOptions = currentTemplate.compilerOptions ?? {};
6579
+ const currentNodeTransforms = currentCompilerOptions.nodeTransforms ?? [];
6580
+ if (currentNodeTransforms.includes(runtimeNodeTransform)) {
6581
+ return;
6582
+ }
6583
+ api.options = {
6584
+ ...currentOptions,
6585
+ template: {
6586
+ ...currentTemplate,
6587
+ compilerOptions: {
6588
+ ...currentCompilerOptions,
6589
+ prefixIdentifiers: true,
6590
+ nodeTransforms: [
6591
+ ...currentNodeTransforms,
6592
+ runtimeNodeTransform
6593
+ ]
6594
+ }
6595
+ }
6596
+ };
6597
+ }
6598
+ };
6599
+ return { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin };
6266
6600
  }
6267
6601
  function assertNonEmptyString(value, name) {
6268
6602
  if (!value || !value.trim()) {
@@ -6388,7 +6722,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6388
6722
  const semanticNameMap = /* @__PURE__ */ new Map();
6389
6723
  const componentHierarchyMap = /* @__PURE__ */ new Map();
6390
6724
  const vueFilesPathMap = /* @__PURE__ */ new Map();
6391
- const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
6725
+ const { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin } = createVuePluginWithTestIds({
6392
6726
  vueOptions,
6393
6727
  existingIdBehavior,
6394
6728
  nameCollisionBehavior,
@@ -6439,7 +6773,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6439
6773
  const resultPlugins = [
6440
6774
  configPlugin,
6441
6775
  metadataCollectorPlugin,
6442
- ...isNuxt ? [] : [internalVuePlugin],
6776
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6443
6777
  ...supportPlugins
6444
6778
  ];
6445
6779
  if (!generationEnabled) {
@@ -6447,7 +6781,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6447
6781
  return [
6448
6782
  configPlugin,
6449
6783
  metadataCollectorPlugin,
6450
- ...isNuxt ? [] : [internalVuePlugin],
6784
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6451
6785
  virtualModules
6452
6786
  ];
6453
6787
  }