@immense/vue-pom-generator 1.0.32 → 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,38 +1,43 @@
1
- ● # Release Notes: v1.0.32
1
+ ● # Release Notes: v1.0.33
2
2
 
3
3
  ## Highlights
4
4
 
5
- - Fixed handling of missing helpers and keyed ID branches in class generation
6
- - Added robust error tolerance for edge cases in Vue component transformation
7
- - Expanded test coverage with 109 new test lines across generated TypeScript and utility
8
- coverage tests
9
- - Introduced PR release-notes preview comments via GitHub Actions
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
10
12
 
11
13
  ## Changes
12
14
 
13
- **Bug Fixes**
14
- - Tolerate missing helpers and keyed ID branches in class generation logic
15
- (`class-generation/index.ts`, `utils.ts`)
15
+ **Core Transformation**
16
+ - Fixed singleton key test-id inference logic
17
+ - Expanded `transform.ts` with enhanced processing capabilities
16
18
 
17
- **Testing**
18
- - Added comprehensive tests for generated TypeScript output (`tests/generated-tsc.test.ts`: +74
19
- lines)
20
- - Expanded utility function coverage tests (`tests/utils-coverage.test.ts`: +35 lines)
21
-
22
- **CI/Automation**
23
- - Added automated PR release-notes preview comments to pull requests
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`
24
24
 
25
- ## 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
26
29
 
27
- None
30
+ **Infrastructure**
31
+ - Updated plugin creation in `plugin/create-vue-pom-generator-plugins.ts`
32
+ - Minor utility adjustments in `utils.ts`
28
33
 
29
34
  ## Pull Requests Included
30
35
 
31
- - [#1](https://github.com/immense/vue-pom-generator/pull/1) Add PR release-notes preview
32
- comments (@dkattan)
36
+ - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
37
+ by @dkattan
33
38
 
34
39
  ## Testing
35
40
 
36
- Significant test additions: 74 lines in generated TypeScript tests and 35 lines in utility
37
- coverage tests validate the fix for missing helpers and keyed ID branches.
41
+ Added 250+ new test cases across generation metrics, transform logic, and options handling. All
42
+ tests passing.
38
43
 
package/dist/index.cjs CHANGED
@@ -4448,6 +4448,32 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
4448
4448
  return `${content}
4449
4449
  `;
4450
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();
4451
4477
  function createBuildProcessorPlugin(options) {
4452
4478
  const {
4453
4479
  componentHierarchyMap,
@@ -4470,7 +4496,6 @@ function createBuildProcessorPlugin(options) {
4470
4496
  routerModuleShims,
4471
4497
  loggerRef
4472
4498
  } = options;
4473
- let lastGeneratedEntryCount = 0;
4474
4499
  return {
4475
4500
  name: "vue-pom-generator-build",
4476
4501
  // This plugin exists to generate code on build output; it is not needed during dev-server HMR.
@@ -4517,11 +4542,13 @@ function createBuildProcessorPlugin(options) {
4517
4542
  this.addWatchFile(pointerPath);
4518
4543
  },
4519
4544
  buildEnd() {
4520
- const entryCount = componentHierarchyMap.size;
4521
- if (entryCount <= 0) {
4545
+ const metrics = getGenerationMetrics(componentHierarchyMap);
4546
+ if (metrics.entryCount <= 0 || metrics.selectorCount <= 0) {
4522
4547
  return;
4523
4548
  }
4524
- if (entryCount < lastGeneratedEntryCount) {
4549
+ const generationMetricsKey = getGenerationMetricsKey(projectRootRef.current, outDir);
4550
+ const previousMetrics = buildGenerationMetricsByOutputKey.get(generationMetricsKey);
4551
+ if (previousMetrics && isLessRich(metrics, previousMetrics)) {
4525
4552
  return;
4526
4553
  }
4527
4554
  generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
@@ -4539,8 +4566,8 @@ function createBuildProcessorPlugin(options) {
4539
4566
  routerEntry: resolvedRouterEntry,
4540
4567
  routerType
4541
4568
  });
4542
- lastGeneratedEntryCount = entryCount;
4543
- loggerRef.current.info(`generated POMs (${entryCount} entries)`);
4569
+ buildGenerationMetricsByOutputKey.set(generationMetricsKey, metrics);
4570
+ loggerRef.current.info(`generated POMs (${metrics.entryCount} entries, ${metrics.selectorCount} selectors)`);
4544
4571
  },
4545
4572
  closeBundle() {
4546
4573
  loggerRef.current.info("build complete");
@@ -4578,6 +4605,89 @@ function toKebabCaseTag(tag) {
4578
4605
  }
4579
4606
  return result;
4580
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
+ }
4581
4691
  function normalizeSearchRoots(wrapperSearchRoots) {
4582
4692
  const normalized = /* @__PURE__ */ new Set();
4583
4693
  for (const root of wrapperSearchRoots) {
@@ -5245,7 +5355,9 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5245
5355
  }
5246
5356
  }
5247
5357
  const getBestAvailableKeyValue = () => {
5248
- 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);
5249
5361
  if (vForKey) return vForKey;
5250
5362
  return getContainedInSlotDataKeyValue(element, hierarchyMap);
5251
5363
  };
@@ -5402,6 +5514,50 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5402
5514
  });
5403
5515
  return;
5404
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
+ }
5405
5561
  const innerText = getInnerText(element) || null;
5406
5562
  const toDirective = nodeHasToDirective(element);
5407
5563
  if (toDirective) {
@@ -5520,6 +5676,7 @@ function resolveComponentNameFromPath(options) {
5520
5676
  }
5521
5677
  return toPascalCase(path.parse(absFilename).name);
5522
5678
  }
5679
+ const devStartupMetricsByOutputKey = /* @__PURE__ */ new Map();
5523
5680
  function createDevProcessorPlugin(options) {
5524
5681
  const {
5525
5682
  nativeWrappers,
@@ -5713,6 +5870,21 @@ function createDevProcessorPlugin(options) {
5713
5870
  logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
5714
5871
  };
5715
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
+ }
5716
5888
  const t0 = node_perf_hooks.performance.now();
5717
5889
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
5718
5890
  outDir,
@@ -5729,8 +5901,11 @@ function createDevProcessorPlugin(options) {
5729
5901
  routerEntry: resolvedRouterEntry,
5730
5902
  routerType
5731
5903
  });
5904
+ if (reason === "startup") {
5905
+ devStartupMetricsByOutputKey.set(generationMetricsKey, metrics);
5906
+ }
5732
5907
  const t1 = node_perf_hooks.performance.now();
5733
- logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
5908
+ logInfo(`generate(${reason}): components=${metrics.entryCount} selectors=${metrics.selectorCount} in ${formatMs(t1 - t0)}`);
5734
5909
  };
5735
5910
  const initialBuildPromise = (async () => {
5736
5911
  const t0 = node_perf_hooks.performance.now();
@@ -6251,20 +6426,21 @@ function createVuePluginWithTestIds(options) {
6251
6426
  }
6252
6427
  ];
6253
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
+ };
6254
6438
  const templateCompilerOptions = {
6255
6439
  ...userCompilerOptions,
6256
6440
  prefixIdentifiers: true,
6257
6441
  nodeTransforms: [
6258
6442
  ...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
- }
6443
+ runtimeNodeTransform
6268
6444
  ]
6269
6445
  };
6270
6446
  const metadataCollectorPlugin = {
@@ -6303,7 +6479,42 @@ function createVuePluginWithTestIds(options) {
6303
6479
  ...vueOptions,
6304
6480
  template
6305
6481
  });
6306
- 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 };
6307
6518
  }
6308
6519
  function assertNonEmptyString(value, name) {
6309
6520
  if (!value || !value.trim()) {
@@ -6429,7 +6640,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6429
6640
  const semanticNameMap = /* @__PURE__ */ new Map();
6430
6641
  const componentHierarchyMap = /* @__PURE__ */ new Map();
6431
6642
  const vueFilesPathMap = /* @__PURE__ */ new Map();
6432
- const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
6643
+ const { metadataCollectorPlugin, internalVuePlugin, nuxtVueBridgePlugin } = createVuePluginWithTestIds({
6433
6644
  vueOptions,
6434
6645
  existingIdBehavior,
6435
6646
  nameCollisionBehavior,
@@ -6480,7 +6691,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6480
6691
  const resultPlugins = [
6481
6692
  configPlugin,
6482
6693
  metadataCollectorPlugin,
6483
- ...isNuxt ? [] : [internalVuePlugin],
6694
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6484
6695
  ...supportPlugins
6485
6696
  ];
6486
6697
  if (!generationEnabled) {
@@ -6488,7 +6699,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6488
6699
  return [
6489
6700
  configPlugin,
6490
6701
  metadataCollectorPlugin,
6491
- ...isNuxt ? [] : [internalVuePlugin],
6702
+ ...isNuxt ? [nuxtVueBridgePlugin] : [internalVuePlugin],
6492
6703
  virtualModules
6493
6704
  ];
6494
6705
  }