@immense/vue-pom-generator 1.0.31 → 1.0.33

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/RELEASE_NOTES.md CHANGED
@@ -1,57 +1,43 @@
1
- I need to see the actual diff content to understand what the fix addressed. Let me get the file
2
- diffs:
3
-
4
- ● Let me look at specific key changes to understand what was fixed:
5
-
6
- ● Based on the commit and PR details, here are the release notes:
7
-
8
- ---
1
+ # Release Notes: v1.0.33
9
2
 
10
3
  ## Highlights
11
4
 
12
- - Changed default output directory from `tests/playwright/generated` to
13
- `tests/playwright/__generated__` for better convention adherence
14
- - Improved `.gitattributes` generation: now skips managed entries for paths using
15
- `__generated__` heuristic (GitHub Linguist auto-detects these)
16
- - Enhanced slot scope key extraction logic for more robust click handler naming in generated
17
- POMs
18
- - Added comprehensive utility functions for analyzing assignment patterns, object destructuring,
19
- and binding identifiers
5
+ - Fixed singleton key test-id inference to correctly handle edge cases
6
+ - Added comprehensive generation metrics and diagnostics system for better visibility into
7
+ transform operations
8
+ - Introduced new `generation-metrics.ts` module for tracking and reporting code generation
9
+ statistics
10
+ - Expanded transform logic with 192 additional lines of processing logic
11
+ - Enhanced plugin architecture with improved metric collection in dev and build modes
20
12
 
21
13
  ## Changes
22
14
 
23
- **Output Directory Convention**
24
- - Updated default `generation.outDir` to `tests/playwright/__generated__`
25
- - Updated default Playwright fixtures output to use `__generated__` subdirectory
26
- - `.gitattributes` entries are now only generated when using custom output directories outside
27
- `__generated__`
28
-
29
- **Click Handler Naming**
30
- - Improved slot scope key candidate extraction with priority-based selection
31
- - Added support for object destructuring patterns in slot scope analysis
32
- - Enhanced fallback key expression generation for slot scoped elements
33
- - Better handling of assignment patterns, rest elements, and object properties
15
+ **Core Transformation**
16
+ - Fixed singleton key test-id inference logic
17
+ - Expanded `transform.ts` with enhanced processing capabilities
34
18
 
35
- **Code Quality**
36
- - Added test coverage for new utility functions in `class-generation-coverage.test.ts` and
37
- `utils-coverage.test.ts`
38
- - Expanded Babel type guards usage for more precise AST node detection
39
- - Refactored slot scope analysis with dedicated helper functions
19
+ **Metrics & Diagnostics**
20
+ - Added `plugin/support/generation-metrics.ts` for tracking generation statistics
21
+ - Updated `plugin/vue-plugin.ts` with enhanced metrics collection (85 lines modified)
22
+ - Improved diagnostic output in `plugin/support/dev-plugin.ts` and
23
+ `plugin/support/build-plugin.ts`
40
24
 
41
- ## Breaking Changes
25
+ **Testing**
26
+ - Added comprehensive test suite in `tests/generation-metrics.test.ts` (47 tests)
27
+ - Expanded `tests/transform.test.ts` with 131 additional test cases
28
+ - Enhanced `tests/options.test.ts` with 72 new test cases
42
29
 
43
- - **Default output directory** changed from `tests/playwright/generated` to
44
- `tests/playwright/__generated__`. If you're relying on the default, generated files will move to
45
- the new location. To preserve the old behavior, explicitly set `generation.outDir:
46
- "tests/playwright/generated"` in your config.
30
+ **Infrastructure**
31
+ - Updated plugin creation in `plugin/create-vue-pom-generator-plugins.ts`
32
+ - Minor utility adjustments in `utils.ts`
47
33
 
48
34
  ## Pull Requests Included
49
35
 
50
- - [#1 Add PR release-notes preview
51
- comments](https://github.com/immense/vue-pom-generator/pull/1) by @dkattan
36
+ - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
37
+ by @dkattan
52
38
 
53
39
  ## Testing
54
40
 
55
- Added test coverage for new utility functions covering class generation and slot scope analysis
56
- edge cases.
41
+ Added 250+ new test cases across generation metrics, transform logic, and options handling. All
42
+ tests passing.
57
43
 
@@ -407,6 +407,7 @@ interface GenerateContentOptions {
407
407
  customPomDir?: string;
408
408
  customPomImportAliases?: Record<string, string>;
409
409
  customPomClassIdentifierMap?: Record<string, string>;
410
+ customPomAvailableClassIdentifiers?: Set<string>;
410
411
 
411
412
  /** Attribute name to treat as the test id. Defaults to `data-testid`. */
412
413
  testIdAttribute?: string;
@@ -1153,8 +1154,14 @@ function generateViewObjectModelContent(
1153
1154
  return false;
1154
1155
  };
1155
1156
 
1157
+ const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
1158
+ const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? new Set<string>();
1159
+
1156
1160
  const attachmentsForThisClass = customPomAttachments
1157
1161
  .filter((a) => {
1162
+ if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
1163
+ return false;
1164
+
1158
1165
  const scope = a.attachTo ?? "views";
1159
1166
  const scopeOk = isView
1160
1167
  ? (scope === "views" || scope === "both")
@@ -1164,7 +1171,7 @@ function generateViewObjectModelContent(
1164
1171
  return a.attachWhenUsesComponents.some(c => hasChildComponent(c));
1165
1172
  })
1166
1173
  .map(a => ({
1167
- className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
1174
+ className: customPomClassIdentifierMap[a.className]!,
1168
1175
  propertyName: a.propertyName,
1169
1176
  }));
1170
1177
 
@@ -1218,7 +1225,7 @@ function generateViewObjectModelContent(
1218
1225
  content += `\nexport class ${className} extends BasePage {\n`;
1219
1226
 
1220
1227
  const widgetInstances = isView
1221
- ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet)
1228
+ ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers)
1222
1229
  : [];
1223
1230
 
1224
1231
  // For views, `childrenComponentSet` only includes component tags on which we applied a data-testid.
@@ -1489,6 +1496,7 @@ async function generateAggregatedFiles(
1489
1496
  };
1490
1497
 
1491
1498
  const customPomClassIdentifierMap = addCustomPomImports();
1499
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
1492
1500
 
1493
1501
  // Collect any navigation return types referenced by generated methods so we can emit
1494
1502
  // stub classes when the destination view has no generated test ids (and therefore no
@@ -1690,6 +1698,7 @@ async function generateAggregatedFiles(
1690
1698
 
1691
1699
  customPomAttachments: options.customPomAttachments ?? [],
1692
1700
  customPomClassIdentifierMap,
1701
+ customPomAvailableClassIdentifiers,
1693
1702
  testIdAttribute: options.testIdAttribute,
1694
1703
  vueRouterFluentChaining: options.vueRouterFluentChaining,
1695
1704
  routeMetaByComponent: options.routeMetaByComponent,
@@ -1817,7 +1826,11 @@ interface WidgetInstance {
1817
1826
  testId: string;
1818
1827
  }
1819
1828
 
1820
- function getWidgetInstancesForView(componentName: string, dataTestIdSet: Set<IDataTestId>): WidgetInstance[] {
1829
+ function getWidgetInstancesForView(
1830
+ componentName: string,
1831
+ dataTestIdSet: Set<IDataTestId>,
1832
+ availableClassIdentifiers: Set<string>,
1833
+ ): WidgetInstance[] {
1821
1834
  const out: WidgetInstance[] = [];
1822
1835
  const usedPropNames = new Set<string>();
1823
1836
 
@@ -1858,6 +1871,9 @@ function getWidgetInstancesForView(componentName: string, dataTestIdSet: Set<IDa
1858
1871
  continue;
1859
1872
  }
1860
1873
 
1874
+ if (!availableClassIdentifiers.has(className))
1875
+ continue;
1876
+
1861
1877
  // Prefer stripping the view prefix (e.g. PreferencesPage-) for cleaner member names.
1862
1878
  const viewPrefix = `${componentName}-`;
1863
1879
  const descriptorRaw = stem.startsWith(viewPrefix) ? stem.slice(viewPrefix.length) : stem;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyOD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;QAEnC;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;KAC5C,CAAC,CAAC;IAEH,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAuCD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBA2EnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyOD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;QAEnC;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;KAC5C,CAAC,CAAC;IAEH,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAwCD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBA2EnC"}
package/dist/index.cjs CHANGED
@@ -1859,6 +1859,12 @@ function applyResolvedDataTestId(args) {
1859
1859
  const existingIdBehavior = args.existingIdBehavior ?? "preserve";
1860
1860
  const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
1861
1861
  const warn = args.warn;
1862
+ const getBestKeyAccessCandidates = (expr) => {
1863
+ if (!expr) {
1864
+ return [];
1865
+ }
1866
+ return expr.split("??").map((part) => part.trim()).filter(Boolean);
1867
+ };
1862
1868
  let dataTestId = args.preferredGeneratedValue;
1863
1869
  let fromExisting = false;
1864
1870
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
@@ -1881,6 +1887,7 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
1881
1887
  if (existingIdBehavior === "preserve") {
1882
1888
  if (existing.isDynamic) {
1883
1889
  if (existing.template) {
1890
+ const existingTemplate = existing.template;
1884
1891
  if ((existing.templateExpressionCount ?? 0) !== 1) {
1885
1892
  throw new Error(
1886
1893
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
@@ -1891,8 +1898,8 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1891
1898
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
1892
1899
  );
1893
1900
  }
1894
- const hasExact = args.bestKeyPlaceholder && existing.template.includes(args.bestKeyPlaceholder);
1895
- const hasVarAccess = args.bestKeyVariable && existing.template.includes(args.bestKeyVariable);
1901
+ const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
1902
+ const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
1896
1903
  if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
1897
1904
  throw new Error(
1898
1905
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
@@ -3883,14 +3890,18 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3883
3890
  }
3884
3891
  return false;
3885
3892
  };
3893
+ const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
3894
+ const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? /* @__PURE__ */ new Set();
3886
3895
  const attachmentsForThisClass = customPomAttachments.filter((a) => {
3896
+ if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
3897
+ return false;
3887
3898
  const scope = a.attachTo ?? "views";
3888
3899
  const scopeOk = isView ? scope === "views" || scope === "both" : scope === "components" || scope === "both";
3889
3900
  if (!scopeOk)
3890
3901
  return false;
3891
3902
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));
3892
3903
  }).map((a) => ({
3893
- className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
3904
+ className: customPomClassIdentifierMap[a.className],
3894
3905
  propertyName: a.propertyName
3895
3906
  }));
3896
3907
  let content = "";
@@ -3930,7 +3941,7 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3930
3941
  content += `
3931
3942
  export class ${className} extends BasePage {
3932
3943
  `;
3933
- const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet) : [];
3944
+ const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
3934
3945
  const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
3935
3946
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
3936
3947
  content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
@@ -4088,6 +4099,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4088
4099
  return customPomClassIdentifierMap2;
4089
4100
  };
4090
4101
  const customPomClassIdentifierMap = addCustomPomImports();
4102
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
4091
4103
  const referencedTargets = /* @__PURE__ */ new Set();
4092
4104
  for (const [, deps] of items) {
4093
4105
  for (const dt of deps.dataTestIdSet) {
@@ -4240,6 +4252,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4240
4252
  aggregated: true,
4241
4253
  customPomAttachments: options.customPomAttachments ?? [],
4242
4254
  customPomClassIdentifierMap,
4255
+ customPomAvailableClassIdentifiers,
4243
4256
  testIdAttribute: options.testIdAttribute,
4244
4257
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4245
4258
  routeMetaByComponent: options.routeMetaByComponent
@@ -4334,7 +4347,7 @@ function toPascalCaseLocal(str) {
4334
4347
  return preserveInternalCaps ? upperFirst(word) : upperFirst(word.toLowerCase());
4335
4348
  }).join("");
4336
4349
  }
4337
- function getWidgetInstancesForView(componentName, dataTestIdSet) {
4350
+ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassIdentifiers) {
4338
4351
  const out = [];
4339
4352
  const usedPropNames = /* @__PURE__ */ new Set();
4340
4353
  const ensureUnique = (base) => {
@@ -4365,6 +4378,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet) {
4365
4378
  } else {
4366
4379
  continue;
4367
4380
  }
4381
+ if (!availableClassIdentifiers.has(className))
4382
+ continue;
4368
4383
  const viewPrefix = `${componentName}-`;
4369
4384
  const descriptorRaw = stem.startsWith(viewPrefix) ? stem.slice(viewPrefix.length) : stem;
4370
4385
  const descriptorPascal = toPascalCaseLocal(descriptorRaw);
@@ -4433,6 +4448,32 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
4433
4448
  return `${content}
4434
4449
  `;
4435
4450
  }
4451
+ function getGenerationMetrics(componentHierarchyMap) {
4452
+ let selectorCount = 0;
4453
+ let generatedMethodCount = 0;
4454
+ for (const deps of componentHierarchyMap.values()) {
4455
+ selectorCount += deps.dataTestIdSet?.size ?? 0;
4456
+ generatedMethodCount += deps.generatedMethods?.size ?? 0;
4457
+ }
4458
+ return {
4459
+ entryCount: componentHierarchyMap.size,
4460
+ selectorCount,
4461
+ generatedMethodCount
4462
+ };
4463
+ }
4464
+ function isLessRich(current, previous) {
4465
+ if (current.entryCount !== previous.entryCount) {
4466
+ return current.entryCount < previous.entryCount;
4467
+ }
4468
+ if (current.selectorCount !== previous.selectorCount) {
4469
+ return current.selectorCount < previous.selectorCount;
4470
+ }
4471
+ return current.generatedMethodCount < previous.generatedMethodCount;
4472
+ }
4473
+ function getGenerationMetricsKey(projectRoot, outDir) {
4474
+ return path.resolve(projectRoot, outDir ?? "./pom");
4475
+ }
4476
+ const buildGenerationMetricsByOutputKey = /* @__PURE__ */ new Map();
4436
4477
  function createBuildProcessorPlugin(options) {
4437
4478
  const {
4438
4479
  componentHierarchyMap,
@@ -4455,7 +4496,6 @@ function createBuildProcessorPlugin(options) {
4455
4496
  routerModuleShims,
4456
4497
  loggerRef
4457
4498
  } = options;
4458
- let lastGeneratedEntryCount = 0;
4459
4499
  return {
4460
4500
  name: "vue-pom-generator-build",
4461
4501
  // This plugin exists to generate code on build output; it is not needed during dev-server HMR.
@@ -4502,11 +4542,13 @@ function createBuildProcessorPlugin(options) {
4502
4542
  this.addWatchFile(pointerPath);
4503
4543
  },
4504
4544
  buildEnd() {
4505
- const entryCount = componentHierarchyMap.size;
4506
- if (entryCount <= 0) {
4545
+ const metrics = getGenerationMetrics(componentHierarchyMap);
4546
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
4507
4547
  return;
4508
4548
  }
4509
- if (entryCount < lastGeneratedEntryCount) {
4549
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
4550
+ const previousMetrics = buildGenerationMetricsByOutputKey.get(generationMetricsKey);
4551
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
4510
4552
  return;
4511
4553
  }
4512
4554
  generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
@@ -4524,8 +4566,8 @@ function createBuildProcessorPlugin(options) {
4524
4566
  routerEntry: resolvedRouterEntry,
4525
4567
  routerType
4526
4568
  });
4527
- lastGeneratedEntryCount = entryCount;
4528
- loggerRef.current.info(`generated POMs (${entryCount} entries)`);
4569
+ buildGenerationMetricsByOutputKey.set(generationMetricsKey, metrics);
4570
+ loggerRef.current.info(`generated POMs (${metrics.entryCount} entries, ${metrics.selectorCount} selectors)`);
4529
4571
  },
4530
4572
  closeBundle() {
4531
4573
  loggerRef.current.info("build complete");
@@ -4563,6 +4605,89 @@ function toKebabCaseTag(tag) {
4563
4605
  }
4564
4606
  return result;
4565
4607
  }
4608
+ function getStaticAttributeContent(element, name) {
4609
+ const attr = element.props.find((prop) => {
4610
+ return prop.type === compilerCore.NodeTypes.ATTRIBUTE && prop.name === name;
4611
+ });
4612
+ return attr?.value?.content?.trim() || null;
4613
+ }
4614
+ function getNativeHtmlControlRole(element) {
4615
+ const tag = (element.tag || "").toLowerCase();
4616
+ const type = (getStaticAttributeContent(element, "type") || "").toLowerCase();
4617
+ if (tag === "textarea") {
4618
+ return "input";
4619
+ }
4620
+ if (tag === "select") {
4621
+ return "select";
4622
+ }
4623
+ if (tag !== "input") {
4624
+ return null;
4625
+ }
4626
+ if (type === "radio") {
4627
+ return "radio";
4628
+ }
4629
+ if (type === "checkbox") {
4630
+ return "checkbox";
4631
+ }
4632
+ return "input";
4633
+ }
4634
+ function normalizeControlLabelText(value) {
4635
+ const normalized = (value ?? "").replace(/\*/g, " ").replace(/\s+/g, " ").trim();
4636
+ return normalized || null;
4637
+ }
4638
+ function getLabelNodeText(labelNode) {
4639
+ for (const child of labelNode.children || []) {
4640
+ if (child.type === compilerCore.NodeTypes.TEXT) {
4641
+ const normalized2 = normalizeControlLabelText(child.content);
4642
+ if (normalized2) {
4643
+ return normalized2;
4644
+ }
4645
+ continue;
4646
+ }
4647
+ if (child.type !== compilerCore.NodeTypes.ELEMENT) {
4648
+ continue;
4649
+ }
4650
+ if (getNativeHtmlControlRole(child)) {
4651
+ continue;
4652
+ }
4653
+ const normalized = normalizeControlLabelText(getInnerText(child));
4654
+ if (normalized) {
4655
+ return normalized;
4656
+ }
4657
+ }
4658
+ return normalizeControlLabelText(getInnerText(labelNode));
4659
+ }
4660
+ function getAssociatedLabelText(element, hierarchyMap2) {
4661
+ let parent = hierarchyMap2.get(element) || null;
4662
+ while (parent) {
4663
+ if (parent.tag === "label") {
4664
+ return getLabelNodeText(parent);
4665
+ }
4666
+ parent = hierarchyMap2.get(parent) || null;
4667
+ }
4668
+ const id = getStaticAttributeContent(element, "id");
4669
+ if (!id) {
4670
+ return null;
4671
+ }
4672
+ const candidates = /* @__PURE__ */ new Set();
4673
+ for (const child of hierarchyMap2.keys()) {
4674
+ candidates.add(child);
4675
+ }
4676
+ for (const maybeParent of hierarchyMap2.values()) {
4677
+ if (maybeParent) {
4678
+ candidates.add(maybeParent);
4679
+ }
4680
+ }
4681
+ for (const candidate of candidates) {
4682
+ if (candidate.tag !== "label") {
4683
+ continue;
4684
+ }
4685
+ if (getStaticAttributeContent(candidate, "for") === id) {
4686
+ return getLabelNodeText(candidate);
4687
+ }
4688
+ }
4689
+ return null;
4690
+ }
4566
4691
  function normalizeSearchRoots(wrapperSearchRoots) {
4567
4692
  const normalized = /* @__PURE__ */ new Set();
4568
4693
  for (const root of wrapperSearchRoots) {
@@ -5230,7 +5355,9 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5230
5355
  }
5231
5356
  }
5232
5357
  const getBestAvailableKeyValue = () => {
5233
- const vForKey = getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5358
+ const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
5359
+ const isDirectVForChild = parentNode?.type === compilerCore.NodeTypes.FOR;
5360
+ const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
5234
5361
  if (vForKey) return vForKey;
5235
5362
  return getContainedInSlotDataKeyValue(element, hierarchyMap);
5236
5363
  };
@@ -5387,6 +5514,50 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5387
5514
  });
5388
5515
  return;
5389
5516
  }
5517
+ const nativeHtmlRole = getNativeHtmlControlRole(element);
5518
+ if (nativeHtmlRole) {
5519
+ const rawIdentifier = getStaticAttributeContent(element, "id") || getStaticAttributeContent(element, "name");
5520
+ const labelText = getAssociatedLabelText(element, hierarchyMap);
5521
+ const { vModel, modelValue } = getModelBindingValues(element);
5522
+ const bindingHint = modelValue || vModel || null;
5523
+ const labelToken = labelText ? toPascalCase(labelText) : "";
5524
+ const bindingToken = bindingHint ? toPascalCase(bindingHint) : "";
5525
+ let identifierToken = null;
5526
+ let semanticNameHint2;
5527
+ if (nativeHtmlRole === "radio" || nativeHtmlRole === "checkbox") {
5528
+ if (rawIdentifier) {
5529
+ identifierToken = rawIdentifier;
5530
+ semanticNameHint2 = rawIdentifier;
5531
+ } else if (bindingToken && labelToken) {
5532
+ identifierToken = `${bindingToken}${labelToken}`;
5533
+ semanticNameHint2 = `${bindingHint || bindingToken} ${labelText || labelToken}`;
5534
+ } else if (labelToken) {
5535
+ identifierToken = labelToken;
5536
+ semanticNameHint2 = labelText || labelToken;
5537
+ } else if (bindingToken) {
5538
+ identifierToken = bindingToken;
5539
+ semanticNameHint2 = bindingHint || bindingToken;
5540
+ }
5541
+ } else if (rawIdentifier) {
5542
+ identifierToken = rawIdentifier;
5543
+ semanticNameHint2 = rawIdentifier;
5544
+ } else if (labelToken) {
5545
+ identifierToken = labelToken;
5546
+ semanticNameHint2 = labelText || labelToken;
5547
+ } else if (bindingToken) {
5548
+ identifierToken = bindingToken;
5549
+ semanticNameHint2 = bindingHint || bindingToken;
5550
+ }
5551
+ if (identifierToken) {
5552
+ const preferredGeneratedValue = bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}-${identifierToken}-${nativeHtmlRole}`) : staticAttributeValue(`${componentName}-${identifierToken}-${nativeHtmlRole}`);
5553
+ applyResolvedDataTestIdForElement({
5554
+ preferredGeneratedValue,
5555
+ nativeRoleOverride: nativeHtmlRole,
5556
+ semanticNameHint: semanticNameHint2 || conditionalHint || void 0
5557
+ });
5558
+ return;
5559
+ }
5560
+ }
5390
5561
  const innerText = getInnerText(element) || null;
5391
5562
  const toDirective = nodeHasToDirective(element);
5392
5563
  if (toDirective) {
@@ -5505,6 +5676,7 @@ function resolveComponentNameFromPath(options) {
5505
5676
  }
5506
5677
  return toPascalCase(path.parse(absFilename).name);
5507
5678
  }
5679
+ const devStartupMetricsByOutputKey = /* @__PURE__ */ new Map();
5508
5680
  function createDevProcessorPlugin(options) {
5509
5681
  const {
5510
5682
  nativeWrappers,
@@ -5698,6 +5870,21 @@ function createDevProcessorPlugin(options) {
5698
5870
  logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
5699
5871
  };
5700
5872
  const generateAggregatedFromSnapshot = (reason) => {
5873
+ const metrics = getGenerationMetrics(snapshotHierarchy);
5874
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
5875
+ logInfo(`generate(${reason}): skipped empty snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`);
5876
+ return;
5877
+ }
5878
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
5879
+ if (reason === "startup") {
5880
+ const previousMetrics = devStartupMetricsByOutputKey.get(generationMetricsKey);
5881
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
5882
+ logInfo(
5883
+ `generate(${reason}): skipped smaller snapshot (components=${metrics.entryCount}, selectors=${metrics.selectorCount})`
5884
+ );
5885
+ return;
5886
+ }
5887
+ }
5701
5888
  const t0 = node_perf_hooks.performance.now();
5702
5889
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
5703
5890
  outDir,
@@ -5714,8 +5901,11 @@ function createDevProcessorPlugin(options) {
5714
5901
  routerEntry: resolvedRouterEntry,
5715
5902
  routerType
5716
5903
  });
5904
+ if (reason === "startup") {
5905
+ devStartupMetricsByOutputKey.set(generationMetricsKey, metrics);
5906
+ }
5717
5907
  const t1 = node_perf_hooks.performance.now();
5718
- logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
5908
+ logInfo(`generate(${reason}): components=${metrics.entryCount} selectors=${metrics.selectorCount} in ${formatMs(t1 - t0)}`);
5719
5909
  };
5720
5910
  const initialBuildPromise = (async () => {
5721
5911
  const t0 = node_perf_hooks.performance.now();
@@ -6236,20 +6426,21 @@ function createVuePluginWithTestIds(options) {
6236
6426
  }
6237
6427
  ];
6238
6428
  };
6429
+ const runtimeNodeTransform = (node, context) => {
6430
+ const filename = context.filename;
6431
+ if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6432
+ return;
6433
+ }
6434
+ const transforms = getNodeTransforms(filename);
6435
+ const ourTransform = transforms[transforms.length - 1];
6436
+ return ourTransform(node, context);
6437
+ };
6239
6438
  const templateCompilerOptions = {
6240
6439
  ...userCompilerOptions,
6241
6440
  prefixIdentifiers: true,
6242
6441
  nodeTransforms: [
6243
6442
  ...userNodeTransforms,
6244
- (node, context) => {
6245
- const filename = context.filename;
6246
- if (!filename || !filename.endsWith(".vue") || !isFileInScope(filename)) {
6247
- return;
6248
- }
6249
- const transforms = getNodeTransforms(filename);
6250
- const ourTransform = transforms[transforms.length - 1];
6251
- return ourTransform(node, context);
6252
- }
6443
+ runtimeNodeTransform
6253
6444
  ]
6254
6445
  };
6255
6446
  const metadataCollectorPlugin = {
@@ -6288,7 +6479,42 @@ function createVuePluginWithTestIds(options) {
6288
6479
  ...vueOptions,
6289
6480
  template
6290
6481
  });
6291
- return { metadataCollectorPlugin, internalVuePlugin };
6482
+ const nuxtVueBridgePlugin = {
6483
+ name: "vue-pom-generator-nuxt-vue-bridge",
6484
+ apply: "serve",
6485
+ configResolved(config) {
6486
+ const viteVuePlugin = config.plugins.find((plugin) => {
6487
+ return typeof plugin === "object" && plugin !== null && "name" in plugin && plugin.name === "vite:vue" && "api" in plugin;
6488
+ });
6489
+ const api = viteVuePlugin?.api;
6490
+ if (!api) {
6491
+ loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
6492
+ return;
6493
+ }
6494
+ const currentOptions = api.options ?? {};
6495
+ const currentTemplate = currentOptions.template ?? {};
6496
+ const currentCompilerOptions = currentTemplate.compilerOptions ?? {};
6497
+ const currentNodeTransforms = currentCompilerOptions.nodeTransforms ?? [];
6498
+ if (currentNodeTransforms.includes(runtimeNodeTransform)) {
6499
+ return;
6500
+ }
6501
+ api.options = {
6502
+ ...currentOptions,
6503
+ template: {
6504
+ ...currentTemplate,
6505
+ compilerOptions: {
6506
+ ...currentCompilerOptions,
6507
+ prefixIdentifiers: true,
6508
+ nodeTransforms: [
6509
+ ...currentNodeTransforms,
6510
+ runtimeNodeTransform
6511
+ ]
6512
+ }
6513
+ }
6514
+ };
6515
+ }
6516
+ };
6517
+ return { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin };
6292
6518
  }
6293
6519
  function assertNonEmptyString(value, name) {
6294
6520
  if (!value || !value.trim()) {
@@ -6414,7 +6640,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6414
6640
  const semanticNameMap = /* @__PURE__ */ new Map();
6415
6641
  const componentHierarchyMap = /* @__PURE__ */ new Map();
6416
6642
  const vueFilesPathMap = /* @__PURE__ */ new Map();
6417
- const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
6643
+ const { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin } = createVuePluginWithTestIds({
6418
6644
  vueOptions,
6419
6645
  existingIdBehavior,
6420
6646
  nameCollisionBehavior,
@@ -6465,7 +6691,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6465
6691
  const resultPlugins = [
6466
6692
  configPlugin,
6467
6693
  metadataCollectorPlugin,
6468
- ...isNuxt ? [] : [internalVuePlugin],
6694
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6469
6695
  ...supportPlugins
6470
6696
  ];
6471
6697
  if (!generationEnabled) {
@@ -6473,7 +6699,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6473
6699
  return [
6474
6700
  configPlugin,
6475
6701
  metadataCollectorPlugin,
6476
- ...isNuxt ? [] : [internalVuePlugin],
6702
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6477
6703
  virtualModules
6478
6704
  ];
6479
6705
  }