@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.cjs CHANGED
@@ -2569,6 +2569,53 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2569
2569
  registerGeneratedMethodSignature(generatedName, signature);
2570
2570
  }
2571
2571
  }
2572
+ function safeRealpath(value) {
2573
+ try {
2574
+ if (fs.existsSync(value)) {
2575
+ return fs.realpathSync(value);
2576
+ }
2577
+ } catch {
2578
+ return value;
2579
+ }
2580
+ const parent = path.dirname(value);
2581
+ if (!parent || parent === value) {
2582
+ return value;
2583
+ }
2584
+ const resolvedParent = safeRealpath(parent);
2585
+ return resolvedParent === parent ? value : path.join(resolvedParent, path.basename(value));
2586
+ }
2587
+ function resolveComponentNameFromPath(options) {
2588
+ const { projectRoot, viewsDirAbs, scanDirs, extraRoots = [] } = options;
2589
+ const cleanFilename = options.filename.includes("?") ? options.filename.substring(0, options.filename.indexOf("?")) : options.filename;
2590
+ const absFilename = path.isAbsolute(cleanFilename) ? cleanFilename : path.resolve(projectRoot, cleanFilename);
2591
+ const normalizedAbsFilename = path.normalize(safeRealpath(absFilename));
2592
+ const rootBases = [projectRoot, ...extraRoots.filter((r) => r !== projectRoot)];
2593
+ const roots = [viewsDirAbs, ...rootBases.flatMap((base) => scanDirs.map((d) => path.resolve(base, d)))];
2594
+ for (const base of rootBases) {
2595
+ for (const dir of scanDirs) {
2596
+ const absDir = path.resolve(base, dir);
2597
+ try {
2598
+ const pagesDir = path.join(absDir, "pages");
2599
+ if (fs.existsSync(pagesDir))
2600
+ roots.push(pagesDir);
2601
+ const componentsDir = path.join(absDir, "components");
2602
+ if (fs.existsSync(componentsDir))
2603
+ roots.push(componentsDir);
2604
+ } catch {
2605
+ }
2606
+ }
2607
+ }
2608
+ const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(safeRealpath(r))))).sort((a, b) => b.length - a.length);
2609
+ for (const root of potentialRoots) {
2610
+ if (normalizedAbsFilename.startsWith(root + path.sep) || normalizedAbsFilename === root) {
2611
+ const rel = path.relative(root, normalizedAbsFilename);
2612
+ const parsed = path.parse(rel);
2613
+ const segments = path.join(parsed.dir, parsed.name);
2614
+ return toPascalCase(segments);
2615
+ }
2616
+ }
2617
+ return toPascalCase(path.parse(normalizedAbsFilename).name);
2618
+ }
2572
2619
  let routerIntrospectionQueue = Promise.resolve();
2573
2620
  async function runRouterIntrospectionExclusive(fn) {
2574
2621
  const prev = routerIntrospectionQueue.catch(() => void 0);
@@ -2848,20 +2895,84 @@ function getRouteParamMeta(router, record, paramNames) {
2848
2895
  }
2849
2896
  });
2850
2897
  }
2851
- function getComponentNameFromRouteRecord(record) {
2852
- const comp = record.components?.default;
2853
- if (!comp)
2898
+ function normalizeRouteComponentFilePath(filePath, options = {}) {
2899
+ const queryIndex = filePath.indexOf("?");
2900
+ const cleanPath = queryIndex === -1 ? filePath : filePath.slice(0, queryIndex);
2901
+ if (cleanPath.startsWith("/@fs/")) {
2902
+ return path.normalize(cleanPath.slice("/@fs/".length));
2903
+ }
2904
+ if (path.isAbsolute(cleanPath)) {
2905
+ if (fs.existsSync(cleanPath) || !options.rootDir)
2906
+ return path.normalize(cleanPath);
2907
+ return path.normalize(path.resolve(options.rootDir, `.${cleanPath}`));
2908
+ }
2909
+ if (!options.rootDir)
2854
2910
  return null;
2911
+ return path.normalize(path.resolve(options.rootDir, cleanPath));
2912
+ }
2913
+ function getComponentInfoFromVueComponent(comp, options = {}) {
2914
+ if (!comp) {
2915
+ return {
2916
+ componentName: null,
2917
+ filePath: null
2918
+ };
2919
+ }
2920
+ let componentName = null;
2921
+ let filePath = null;
2855
2922
  if (typeof comp.__file === "string" && comp.__file.length) {
2923
+ filePath = normalizeRouteComponentFilePath(comp.__file, { rootDir: options.rootDir });
2856
2924
  const base = path.posix.basename(path.posix.normalize(comp.__file));
2857
2925
  if (base.toLowerCase().endsWith(".vue"))
2858
- return base.slice(0, -".vue".length);
2926
+ componentName = base.slice(0, -".vue".length);
2859
2927
  }
2860
- if (typeof comp.__name === "string" && comp.__name.length)
2861
- return comp.__name;
2862
- if (typeof comp.name === "string" && comp.name.length)
2863
- return comp.name;
2864
- return null;
2928
+ if (!componentName && typeof comp.__name === "string" && comp.__name.length)
2929
+ componentName = comp.__name;
2930
+ if (!componentName && options.allowFunctionNameFallback !== false && typeof comp.name === "string" && comp.name.length) {
2931
+ componentName = comp.name;
2932
+ }
2933
+ return {
2934
+ componentName,
2935
+ filePath
2936
+ };
2937
+ }
2938
+ async function getComponentInfoFromRouteRecord(record, options = {}) {
2939
+ const comp = record.components?.default;
2940
+ if (!comp) {
2941
+ return {
2942
+ componentName: null,
2943
+ filePath: null
2944
+ };
2945
+ }
2946
+ if (typeof comp !== "function") {
2947
+ return getComponentInfoFromVueComponent(comp, options);
2948
+ }
2949
+ const directInfo = getComponentInfoFromVueComponent(comp, {
2950
+ allowFunctionNameFallback: false,
2951
+ rootDir: options.rootDir
2952
+ });
2953
+ if (directInfo.componentName || directInfo.filePath)
2954
+ return directInfo;
2955
+ try {
2956
+ const loaded = await comp();
2957
+ const resolved = loaded && typeof loaded === "object" && "default" in loaded ? loaded.default : loaded;
2958
+ const loadedInfo = getComponentInfoFromVueComponent(resolved, options);
2959
+ if (loadedInfo.componentName || loadedInfo.filePath)
2960
+ return loadedInfo;
2961
+ } catch {
2962
+ }
2963
+ return getComponentInfoFromVueComponent(comp, options);
2964
+ }
2965
+ function resolveIntrospectedComponentName(componentInfo, componentNaming) {
2966
+ if (componentInfo.filePath && componentNaming) {
2967
+ return resolveComponentNameFromPath({
2968
+ filename: componentInfo.filePath,
2969
+ projectRoot: componentNaming.projectRoot,
2970
+ viewsDirAbs: componentNaming.viewsDirAbs,
2971
+ scanDirs: componentNaming.scanDirs,
2972
+ extraRoots: componentNaming.extraRoots
2973
+ });
2974
+ }
2975
+ return componentInfo.componentName;
2865
2976
  }
2866
2977
  async function ensureDomShim() {
2867
2978
  const domShimHtml = "<!doctype html><html><head></head><body><div id='app'></div></body></html>";
@@ -3107,7 +3218,8 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
3107
3218
  const routePathMap = /* @__PURE__ */ new Map();
3108
3219
  const routeMetaEntries = [];
3109
3220
  for (const r of router.getRoutes()) {
3110
- const componentName = getComponentNameFromRouteRecord(r);
3221
+ const componentInfo = await getComponentInfoFromRouteRecord(r, { rootDir: cwd });
3222
+ const componentName = resolveIntrospectedComponentName(componentInfo, options.componentNaming);
3111
3223
  if (!componentName)
3112
3224
  continue;
3113
3225
  if (typeof r.path === "string" && r.path.length) {
@@ -3165,9 +3277,20 @@ function resolveRouterEntry(projectRoot, routerEntry) {
3165
3277
  const root = projectRoot ?? process.cwd();
3166
3278
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
3167
3279
  }
3168
- async function getRouteMetaByComponent(projectRoot, routerEntry, routerType) {
3280
+ async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, options = {}) {
3169
3281
  const root = projectRoot ?? process.cwd();
3170
- const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry));
3282
+ const viewsDir = options.viewsDir ?? "src/views";
3283
+ const viewsDirAbs = path.isAbsolute(viewsDir) ? viewsDir : path.resolve(root, viewsDir);
3284
+ const scanDirs = options.scanDirs?.length ? options.scanDirs : ["src"];
3285
+ const extraRoots = process.cwd() !== root ? [process.cwd()] : [];
3286
+ const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry), {
3287
+ componentNaming: {
3288
+ projectRoot: root,
3289
+ viewsDirAbs,
3290
+ scanDirs,
3291
+ extraRoots
3292
+ }
3293
+ });
3171
3294
  const map = /* @__PURE__ */ new Map();
3172
3295
  for (const entry of routeMetaEntries) {
3173
3296
  const list = map.get(entry.componentName) ?? [];
@@ -3344,11 +3467,17 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3344
3467
  csharp,
3345
3468
  vueRouterFluentChaining,
3346
3469
  routerEntry,
3347
- routerType
3470
+ routerType,
3471
+ viewsDir,
3472
+ scanDirs,
3473
+ routeMetaByComponent: routeMetaByComponentOverride
3348
3474
  } = options;
3349
3475
  const emitLanguages = emitLanguagesOverride?.length ? emitLanguagesOverride : ["ts"];
3350
3476
  const outDir = outDirOverride ?? "./pom";
3351
- const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType) : void 0;
3477
+ const routeMetaByComponent = routeMetaByComponentOverride ?? (vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType, {
3478
+ viewsDir,
3479
+ scanDirs
3480
+ }) : void 0);
3352
3481
  const generatedFilePaths = [];
3353
3482
  const writeGeneratedFile = (file) => {
3354
3483
  const resolvedFilePath = path.resolve(file.filePath);
@@ -4448,10 +4577,38 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
4448
4577
  return `${content}
4449
4578
  `;
4450
4579
  }
4580
+ function getGenerationMetrics(componentHierarchyMap) {
4581
+ let selectorCount = 0;
4582
+ let generatedMethodCount = 0;
4583
+ for (const deps of componentHierarchyMap.values()) {
4584
+ selectorCount += deps.dataTestIdSet?.size ?? 0;
4585
+ generatedMethodCount += deps.generatedMethods?.size ?? 0;
4586
+ }
4587
+ return {
4588
+ entryCount: componentHierarchyMap.size,
4589
+ selectorCount,
4590
+ generatedMethodCount
4591
+ };
4592
+ }
4593
+ function isLessRich(current, previous) {
4594
+ if (current.entryCount !== previous.entryCount) {
4595
+ return current.entryCount < previous.entryCount;
4596
+ }
4597
+ if (current.selectorCount !== previous.selectorCount) {
4598
+ return current.selectorCount < previous.selectorCount;
4599
+ }
4600
+ return current.generatedMethodCount < previous.generatedMethodCount;
4601
+ }
4602
+ function getGenerationMetricsKey(projectRoot, outDir) {
4603
+ return path.resolve(projectRoot, outDir ?? "./pom");
4604
+ }
4605
+ const buildGenerationMetricsByOutputKey = /* @__PURE__ */ new Map();
4451
4606
  function createBuildProcessorPlugin(options) {
4452
4607
  const {
4453
4608
  componentHierarchyMap,
4454
4609
  vueFilesPathMap,
4610
+ viewsDir,
4611
+ scanDirs,
4455
4612
  basePageClassPath,
4456
4613
  normalizedBasePagePath,
4457
4614
  outDir,
@@ -4470,7 +4627,6 @@ function createBuildProcessorPlugin(options) {
4470
4627
  routerModuleShims,
4471
4628
  loggerRef
4472
4629
  } = options;
4473
- let lastGeneratedEntryCount = 0;
4474
4630
  return {
4475
4631
  name: "vue-pom-generator-build",
4476
4632
  // This plugin exists to generate code on build output; it is not needed during dev-server HMR.
@@ -4488,7 +4644,14 @@ function createBuildProcessorPlugin(options) {
4488
4644
  } else {
4489
4645
  if (!resolvedRouterEntry)
4490
4646
  throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
4491
- result = await parseRouterFileFromCwd(resolvedRouterEntry, { moduleShims: routerModuleShims });
4647
+ result = await parseRouterFileFromCwd(resolvedRouterEntry, {
4648
+ moduleShims: routerModuleShims,
4649
+ componentNaming: {
4650
+ projectRoot: projectRootRef.current,
4651
+ viewsDirAbs: path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir),
4652
+ scanDirs
4653
+ }
4654
+ });
4492
4655
  }
4493
4656
  const { routeNameMap, routePathMap } = result;
4494
4657
  setRouteNameToComponentNameMap(routeNameMap);
@@ -4517,11 +4680,13 @@ function createBuildProcessorPlugin(options) {
4517
4680
  this.addWatchFile(pointerPath);
4518
4681
  },
4519
4682
  buildEnd() {
4520
- const entryCount = componentHierarchyMap.size;
4521
- if (entryCount <= 0) {
4683
+ const metrics = getGenerationMetrics(componentHierarchyMap);
4684
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
4522
4685
  return;
4523
4686
  }
4524
- if (entryCount < lastGeneratedEntryCount) {
4687
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
4688
+ const previousMetrics = buildGenerationMetricsByOutputKey.get(generationMetricsKey);
4689
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
4525
4690
  return;
4526
4691
  }
4527
4692
  generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
@@ -4537,10 +4702,12 @@ function createBuildProcessorPlugin(options) {
4537
4702
  testIdAttribute,
4538
4703
  vueRouterFluentChaining: routerAwarePoms,
4539
4704
  routerEntry: resolvedRouterEntry,
4540
- routerType
4705
+ routerType,
4706
+ viewsDir,
4707
+ scanDirs
4541
4708
  });
4542
- lastGeneratedEntryCount = entryCount;
4543
- loggerRef.current.info(`generated POMs (${entryCount} entries)`);
4709
+ buildGenerationMetricsByOutputKey.set(generationMetricsKey, metrics);
4710
+ loggerRef.current.info(`generated POMs (${metrics.entryCount} entries, ${metrics.selectorCount} selectors)`);
4544
4711
  },
4545
4712
  closeBundle() {
4546
4713
  loggerRef.current.info("build complete");
@@ -4578,6 +4745,89 @@ function toKebabCaseTag(tag) {
4578
4745
  }
4579
4746
  return result;
4580
4747
  }
4748
+ function getStaticAttributeContent(element, name) {
4749
+ const attr = element.props.find((prop) => {
4750
+ return prop.type === compilerCore.NodeTypes.ATTRIBUTE && prop.name === name;
4751
+ });
4752
+ return attr?.value?.content?.trim() || null;
4753
+ }
4754
+ function getNativeHtmlControlRole(element) {
4755
+ const tag = (element.tag || "").toLowerCase();
4756
+ const type = (getStaticAttributeContent(element, "type") || "").toLowerCase();
4757
+ if (tag === "textarea") {
4758
+ return "input";
4759
+ }
4760
+ if (tag === "select") {
4761
+ return "select";
4762
+ }
4763
+ if (tag !== "input") {
4764
+ return null;
4765
+ }
4766
+ if (type === "radio") {
4767
+ return "radio";
4768
+ }
4769
+ if (type === "checkbox") {
4770
+ return "checkbox";
4771
+ }
4772
+ return "input";
4773
+ }
4774
+ function normalizeControlLabelText(value) {
4775
+ const normalized = (value ?? "").replace(/\*/g, " ").replace(/\s+/g, " ").trim();
4776
+ return normalized || null;
4777
+ }
4778
+ function getLabelNodeText(labelNode) {
4779
+ for (const child of labelNode.children || []) {
4780
+ if (child.type === compilerCore.NodeTypes.TEXT) {
4781
+ const normalized2 = normalizeControlLabelText(child.content);
4782
+ if (normalized2) {
4783
+ return normalized2;
4784
+ }
4785
+ continue;
4786
+ }
4787
+ if (child.type !== compilerCore.NodeTypes.ELEMENT) {
4788
+ continue;
4789
+ }
4790
+ if (getNativeHtmlControlRole(child)) {
4791
+ continue;
4792
+ }
4793
+ const normalized = normalizeControlLabelText(getInnerText(child));
4794
+ if (normalized) {
4795
+ return normalized;
4796
+ }
4797
+ }
4798
+ return normalizeControlLabelText(getInnerText(labelNode));
4799
+ }
4800
+ function getAssociatedLabelText(element, hierarchyMap2) {
4801
+ let parent = hierarchyMap2.get(element) || null;
4802
+ while (parent) {
4803
+ if (parent.tag === "label") {
4804
+ return getLabelNodeText(parent);
4805
+ }
4806
+ parent = hierarchyMap2.get(parent) || null;
4807
+ }
4808
+ const id = getStaticAttributeContent(element, "id");
4809
+ if (!id) {
4810
+ return null;
4811
+ }
4812
+ const candidates = /* @__PURE__ */ new Set();
4813
+ for (const child of hierarchyMap2.keys()) {
4814
+ candidates.add(child);
4815
+ }
4816
+ for (const maybeParent of hierarchyMap2.values()) {
4817
+ if (maybeParent) {
4818
+ candidates.add(maybeParent);
4819
+ }
4820
+ }
4821
+ for (const candidate of candidates) {
4822
+ if (candidate.tag !== "label") {
4823
+ continue;
4824
+ }
4825
+ if (getStaticAttributeContent(candidate, "for") === id) {
4826
+ return getLabelNodeText(candidate);
4827
+ }
4828
+ }
4829
+ return null;
4830
+ }
4581
4831
  function normalizeSearchRoots(wrapperSearchRoots) {
4582
4832
  const normalized = /* @__PURE__ */ new Set();
4583
4833
  for (const root of wrapperSearchRoots) {
@@ -5110,14 +5360,14 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5110
5360
  const warn = options.warn;
5111
5361
  const vueFilesPathMap = options.vueFilesPathMap;
5112
5362
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
5113
- const safeRealpath = (p) => {
5363
+ const safeRealpath2 = (p) => {
5114
5364
  try {
5115
5365
  return fs.existsSync(p) ? fs.realpathSync(p) : p;
5116
5366
  } catch {
5117
5367
  return p;
5118
5368
  }
5119
5369
  };
5120
- const normalizedViewsDirAbs = path.normalize(safeRealpath(path.resolve(viewsDirAbs)));
5370
+ const normalizedViewsDirAbs = path.normalize(safeRealpath2(path.resolve(viewsDirAbs)));
5121
5371
  const generatedMethodContentByComponent = /* @__PURE__ */ new Map();
5122
5372
  const lastConditionalHintByParent = /* @__PURE__ */ new WeakMap();
5123
5373
  const lastConditionalMergeGroupByParent = /* @__PURE__ */ new WeakMap();
@@ -5205,7 +5455,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5205
5455
  const parentIsRoot = context?.parent?.type === compilerCore.NodeTypes.ROOT;
5206
5456
  const parentElement = !parentIsRoot && context?.parent?.type === compilerCore.NodeTypes.ELEMENT ? context.parent : null;
5207
5457
  hierarchyMap.set(element, parentElement);
5208
- const normalizeFilePath = (filePath) => path.normalize(safeRealpath(path.resolve(filePath)));
5458
+ const normalizeFilePath = (filePath) => path.normalize(safeRealpath2(path.resolve(filePath)));
5209
5459
  const normalizedFilePath = normalizeFilePath(context.filename);
5210
5460
  const parentComponentName = componentName;
5211
5461
  const dependencies = (() => {
@@ -5245,7 +5495,9 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5245
5495
  }
5246
5496
  }
5247
5497
  const getBestAvailableKeyValue = () => {
5248
- const vForKey = getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5498
+ const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
5499
+ const isDirectVForChild = parentNode?.type === compilerCore.NodeTypes.FOR;
5500
+ const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5249
5501
  if (vForKey) return vForKey;
5250
5502
  return getContainedInSlotDataKeyValue(element, hierarchyMap);
5251
5503
  };
@@ -5402,6 +5654,50 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5402
5654
  });
5403
5655
  return;
5404
5656
  }
5657
+ const nativeHtmlRole = getNativeHtmlControlRole(element);
5658
+ if (nativeHtmlRole) {
5659
+ const rawIdentifier = getStaticAttributeContent(element, "id") || getStaticAttributeContent(element, "name");
5660
+ const labelText = getAssociatedLabelText(element, hierarchyMap);
5661
+ const { vModel, modelValue } = getModelBindingValues(element);
5662
+ const bindingHint = modelValue || vModel || null;
5663
+ const labelToken = labelText ? toPascalCase(labelText) : "";
5664
+ const bindingToken = bindingHint ? toPascalCase(bindingHint) : "";
5665
+ let identifierToken = null;
5666
+ let semanticNameHint2;
5667
+ if (nativeHtmlRole === "radio" || nativeHtmlRole === "checkbox") {
5668
+ if (rawIdentifier) {
5669
+ identifierToken = rawIdentifier;
5670
+ semanticNameHint2 = rawIdentifier;
5671
+ } else if (bindingToken && labelToken) {
5672
+ identifierToken = `${bindingToken}${labelToken}`;
5673
+ semanticNameHint2 = `${bindingHint || bindingToken} ${labelText || labelToken}`;
5674
+ } else if (labelToken) {
5675
+ identifierToken = labelToken;
5676
+ semanticNameHint2 = labelText || labelToken;
5677
+ } else if (bindingToken) {
5678
+ identifierToken = bindingToken;
5679
+ semanticNameHint2 = bindingHint || bindingToken;
5680
+ }
5681
+ } else if (rawIdentifier) {
5682
+ identifierToken = rawIdentifier;
5683
+ semanticNameHint2 = rawIdentifier;
5684
+ } else if (labelToken) {
5685
+ identifierToken = labelToken;
5686
+ semanticNameHint2 = labelText || labelToken;
5687
+ } else if (bindingToken) {
5688
+ identifierToken = bindingToken;
5689
+ semanticNameHint2 = bindingHint || bindingToken;
5690
+ }
5691
+ if (identifierToken) {
5692
+ const preferredGeneratedValue = bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}-${identifierToken}-${nativeHtmlRole}`) : staticAttributeValue(`${componentName}-${identifierToken}-${nativeHtmlRole}`);
5693
+ applyResolvedDataTestIdForElement({
5694
+ preferredGeneratedValue,
5695
+ nativeRoleOverride: nativeHtmlRole,
5696
+ semanticNameHint: semanticNameHint2 || conditionalHint || void 0
5697
+ });
5698
+ return;
5699
+ }
5700
+ }
5405
5701
  const innerText = getInnerText(element) || null;
5406
5702
  const toDirective = nodeHasToDirective(element);
5407
5703
  if (toDirective) {
@@ -5443,12 +5739,14 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5443
5739
  });
5444
5740
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
5445
5741
  const idOrName = getIdOrName(element) || void 0;
5446
- const semanticNameHint2 = clickHint || idOrName || innerText || conditionalHint || void 0;
5742
+ const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
5743
+ const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
5447
5744
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
5448
5745
  const testId = getClickDataTestId(clickSuffix);
5449
5746
  applyResolvedDataTestIdForElement({
5450
5747
  preferredGeneratedValue: testId,
5451
5748
  semanticNameHint: semanticNameHint2,
5749
+ semanticNameHintAlternates,
5452
5750
  pomMergeKey
5453
5751
  });
5454
5752
  {
@@ -5489,37 +5787,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5489
5787
  }
5490
5788
  };
5491
5789
  }
5492
- function resolveComponentNameFromPath(options) {
5493
- const { projectRoot, viewsDirAbs, scanDirs, extraRoots = [] } = options;
5494
- const cleanFilename = options.filename.includes("?") ? options.filename.substring(0, options.filename.indexOf("?")) : options.filename;
5495
- const absFilename = path.isAbsolute(cleanFilename) ? cleanFilename : path.resolve(projectRoot, cleanFilename);
5496
- const rootBases = [projectRoot, ...extraRoots.filter((r) => r !== projectRoot)];
5497
- const roots = [viewsDirAbs, ...rootBases.flatMap((base) => scanDirs.map((d) => path.resolve(base, d)))];
5498
- for (const base of rootBases) {
5499
- for (const dir of scanDirs) {
5500
- const absDir = path.resolve(base, dir);
5501
- try {
5502
- const pagesDir = path.join(absDir, "pages");
5503
- if (fs.existsSync(pagesDir))
5504
- roots.push(pagesDir);
5505
- const componentsDir = path.join(absDir, "components");
5506
- if (fs.existsSync(componentsDir))
5507
- roots.push(componentsDir);
5508
- } catch {
5509
- }
5510
- }
5511
- }
5512
- const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
5513
- for (const root of potentialRoots) {
5514
- if (absFilename.startsWith(root + path.sep) || absFilename === root) {
5515
- const rel = path.relative(root, absFilename);
5516
- const parsed = path.parse(rel);
5517
- const segments = path.join(parsed.dir, parsed.name);
5518
- return toPascalCase(segments);
5519
- }
5520
- }
5521
- return toPascalCase(path.parse(absFilename).name);
5522
- }
5790
+ const devStartupMetricsByOutputKey = /* @__PURE__ */ new Map();
5523
5791
  function createDevProcessorPlugin(options) {
5524
5792
  const {
5525
5793
  nativeWrappers,
@@ -5565,6 +5833,7 @@ function createDevProcessorPlugin(options) {
5565
5833
  scheduleVueFileRegen(ctx.file, "hmr");
5566
5834
  },
5567
5835
  configureServer(server) {
5836
+ const getViewsDirAbs = () => path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir);
5568
5837
  const routerInitPromise = (async () => {
5569
5838
  if (!routerAwarePoms) {
5570
5839
  setRouteNameToComponentNameMap(/* @__PURE__ */ new Map());
@@ -5577,7 +5846,15 @@ function createDevProcessorPlugin(options) {
5577
5846
  } else {
5578
5847
  if (!resolvedRouterEntry)
5579
5848
  throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
5580
- result = await parseRouterFileFromCwd(resolvedRouterEntry, { moduleShims: routerModuleShims });
5849
+ result = await parseRouterFileFromCwd(resolvedRouterEntry, {
5850
+ moduleShims: routerModuleShims,
5851
+ componentNaming: {
5852
+ projectRoot: projectRootRef.current,
5853
+ viewsDirAbs: getViewsDirAbs(),
5854
+ scanDirs,
5855
+ extraRoots: [process.cwd()]
5856
+ }
5857
+ });
5581
5858
  }
5582
5859
  const { routeNameMap, routePathMap } = result;
5583
5860
  setRouteNameToComponentNameMap(routeNameMap);
@@ -5600,7 +5877,6 @@ function createDevProcessorPlugin(options) {
5600
5877
  const logDebug = (message) => loggerRef.current.debug(message);
5601
5878
  let scheduleVueFileRegenLocal = null;
5602
5879
  const formatMs = (ms) => `${ms.toFixed(1)}ms`;
5603
- const getViewsDirAbs = () => path.isAbsolute(viewsDir) ? viewsDir : path.resolve(projectRootRef.current, viewsDir);
5604
5880
  const extractTemplateFromSfc = (source, filename) => {
5605
5881
  const { descriptor } = compilerSfc.parse(source, {
5606
5882
  filename: filename ?? "anonymous.vue"
@@ -5713,6 +5989,21 @@ function createDevProcessorPlugin(options) {
5713
5989
  logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
5714
5990
  };
5715
5991
  const generateAggregatedFromSnapshot = (reason) => {
5992
+ const metrics = getGenerationMetrics(snapshotHierarchy);
5993
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
5994
+ logInfo(`generate(${reason}): skipped empty snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`);
5995
+ return;
5996
+ }
5997
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
5998
+ if (reason === "startup") {
5999
+ const previousMetrics = devStartupMetricsByOutputKey.get(generationMetricsKey);
6000
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
6001
+ logInfo(
6002
+ `generate(${reason}): skipped smaller snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`
6003
+ );
6004
+ return;
6005
+ }
6006
+ }
5716
6007
  const t0 = node_perf_hooks.performance.now();
5717
6008
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
5718
6009
  outDir,
@@ -5727,10 +6018,15 @@ function createDevProcessorPlugin(options) {
5727
6018
  testIdAttribute,
5728
6019
  vueRouterFluentChaining: routerAwarePoms,
5729
6020
  routerEntry: resolvedRouterEntry,
5730
- routerType
6021
+ routerType,
6022
+ viewsDir,
6023
+ scanDirs
5731
6024
  });
6025
+ if (reason === "startup") {
6026
+ devStartupMetricsByOutputKey.set(generationMetricsKey, metrics);
6027
+ }
5732
6028
  const t1 = node_perf_hooks.performance.now();
5733
- logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
6029
+ logInfo(`generate(${reason}): components=${metrics.entryCount} selectors=${metrics.selectorCount} in ${formatMs(t1 - t0)}`);
5734
6030
  };
5735
6031
  const initialBuildPromise = (async () => {
5736
6032
  const t0 = node_perf_hooks.performance.now();
@@ -5946,6 +6242,8 @@ function createSupportPlugins(options) {
5946
6242
  const tsProcessor = createBuildProcessorPlugin({
5947
6243
  componentHierarchyMap,
5948
6244
  vueFilesPathMap,
6245
+ viewsDir,
6246
+ scanDirs,
5949
6247
  basePageClassPath,
5950
6248
  normalizedBasePagePath,
5951
6249
  outDir,
@@ -6251,20 +6549,21 @@ function createVuePluginWithTestIds(options) {
6251
6549
  }
6252
6550
  ];
6253
6551
  };
6552
+ const runtimeNodeTransform = (node, context) => {
6553
+ const filename = context.filename;
6554
+ if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6555
+ return;
6556
+ }
6557
+ const transforms = getNodeTransforms(filename);
6558
+ const ourTransform = transforms[transforms.length - 1];
6559
+ return ourTransform(node, context);
6560
+ };
6254
6561
  const templateCompilerOptions = {
6255
6562
  ...userCompilerOptions,
6256
6563
  prefixIdentifiers: true,
6257
6564
  nodeTransforms: [
6258
6565
  ...userNodeTransforms,
6259
- (node, context) => {
6260
- const filename = context.filename;
6261
- if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6262
- return;
6263
- }
6264
- const transforms = getNodeTransforms(filename);
6265
- const ourTransform = transforms[transforms.length - 1];
6266
- return ourTransform(node, context);
6267
- }
6566
+ runtimeNodeTransform
6268
6567
  ]
6269
6568
  };
6270
6569
  const metadataCollectorPlugin = {
@@ -6303,7 +6602,42 @@ function createVuePluginWithTestIds(options) {
6303
6602
  ...vueOptions,
6304
6603
  template
6305
6604
  });
6306
- return { metadataCollectorPlugin, internalVuePlugin };
6605
+ const nuxtVueBridgePlugin = {
6606
+ name: "vue-pom-generator-nuxt-vue-bridge",
6607
+ apply: "serve",
6608
+ configResolved(config) {
6609
+ const viteVuePlugin = config.plugins.find((plugin) => {
6610
+ return typeof plugin === "object" && plugin !== null && "name" in plugin && plugin.name === "vite:vue" && "api" in plugin;
6611
+ });
6612
+ const api = viteVuePlugin?.api;
6613
+ if (!api) {
6614
+ loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
6615
+ return;
6616
+ }
6617
+ const currentOptions = api.options ?? {};
6618
+ const currentTemplate = currentOptions.template ?? {};
6619
+ const currentCompilerOptions = currentTemplate.compilerOptions ?? {};
6620
+ const currentNodeTransforms = currentCompilerOptions.nodeTransforms ?? [];
6621
+ if (currentNodeTransforms.includes(runtimeNodeTransform)) {
6622
+ return;
6623
+ }
6624
+ api.options = {
6625
+ ...currentOptions,
6626
+ template: {
6627
+ ...currentTemplate,
6628
+ compilerOptions: {
6629
+ ...currentCompilerOptions,
6630
+ prefixIdentifiers: true,
6631
+ nodeTransforms: [
6632
+ ...currentNodeTransforms,
6633
+ runtimeNodeTransform
6634
+ ]
6635
+ }
6636
+ }
6637
+ };
6638
+ }
6639
+ };
6640
+ return { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin };
6307
6641
  }
6308
6642
  function assertNonEmptyString(value, name) {
6309
6643
  if (!value || !value.trim()) {
@@ -6429,7 +6763,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6429
6763
  const semanticNameMap = /* @__PURE__ */ new Map();
6430
6764
  const componentHierarchyMap = /* @__PURE__ */ new Map();
6431
6765
  const vueFilesPathMap = /* @__PURE__ */ new Map();
6432
- const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
6766
+ const { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin } = createVuePluginWithTestIds({
6433
6767
  vueOptions,
6434
6768
  existingIdBehavior,
6435
6769
  nameCollisionBehavior,
@@ -6480,7 +6814,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6480
6814
  const resultPlugins = [
6481
6815
  configPlugin,
6482
6816
  metadataCollectorPlugin,
6483
- ...isNuxt ? [] : [internalVuePlugin],
6817
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6484
6818
  ...supportPlugins
6485
6819
  ];
6486
6820
  if (!generationEnabled) {
@@ -6488,7 +6822,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6488
6822
  return [
6489
6823
  configPlugin,
6490
6824
  metadataCollectorPlugin,
6491
- ...isNuxt ? [] : [internalVuePlugin],
6825
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6492
6826
  virtualModules
6493
6827
  ];
6494
6828
  }