@immense/vue-pom-generator 1.0.30 → 1.0.32

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
@@ -4,7 +4,7 @@ import { pathToFileURL, fileURLToPath } from "node:url";
4
4
  import fs from "node:fs";
5
5
  import { JSDOM } from "jsdom";
6
6
  import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression } from "@vue/compiler-core";
7
- import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isOptionalMemberExpression, isObjectExpression, isFile, isArrowFunctionExpression, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isObjectProperty, isProgram, VISITOR_KEYS } from "@babel/types";
7
+ import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isArrowFunctionExpression, isOptionalMemberExpression, isObjectExpression, isFile, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
8
8
  import { parseExpression, parse } from "@babel/parser";
9
9
  import { performance } from "node:perf_hooks";
10
10
  import * as compilerDom from "@vue/compiler-dom";
@@ -565,6 +565,124 @@ function getTemplateSlotScope(node) {
565
565
  }
566
566
  return null;
567
567
  }
568
+ function isSimpleScopeIdentifier(value) {
569
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
570
+ }
571
+ function buildSlotScopeFallbackKeyExpression(identifier) {
572
+ return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
573
+ }
574
+ function tryGetBindingIdentifierName(node) {
575
+ if (!node) {
576
+ return null;
577
+ }
578
+ if (isIdentifier(node)) {
579
+ return node.name;
580
+ }
581
+ if (isAssignmentPattern(node)) {
582
+ return tryGetBindingIdentifierName(node.left);
583
+ }
584
+ if (isRestElement(node)) {
585
+ return tryGetBindingIdentifierName(node.argument);
586
+ }
587
+ return null;
588
+ }
589
+ function getSlotScopeObjectPropertyKeyName(node) {
590
+ if (isIdentifier(node)) {
591
+ return node.name;
592
+ }
593
+ if (isStringLiteral(node)) {
594
+ return node.value;
595
+ }
596
+ return null;
597
+ }
598
+ function tryGetSlotScopeKeyCandidate(node) {
599
+ if (!node) {
600
+ return null;
601
+ }
602
+ if (isIdentifier(node)) {
603
+ return {
604
+ priority: 2,
605
+ expression: buildSlotScopeFallbackKeyExpression(node.name)
606
+ };
607
+ }
608
+ if (isAssignmentPattern(node)) {
609
+ return tryGetSlotScopeKeyCandidate(node.left);
610
+ }
611
+ if (isRestElement(node)) {
612
+ return tryGetSlotScopeKeyCandidate(node.argument);
613
+ }
614
+ if (!isObjectPattern(node)) {
615
+ return null;
616
+ }
617
+ let best = null;
618
+ for (const property of node.properties) {
619
+ let candidate = null;
620
+ if (isRestElement(property)) {
621
+ candidate = tryGetSlotScopeKeyCandidate(property.argument);
622
+ if (candidate) {
623
+ candidate = { ...candidate, priority: Math.max(candidate.priority, 2) };
624
+ }
625
+ } else if (isObjectProperty(property)) {
626
+ const keyName = getSlotScopeObjectPropertyKeyName(property.key);
627
+ const bindingName = tryGetBindingIdentifierName(property.value) ?? tryGetBindingIdentifierName(property.key);
628
+ if (bindingName) {
629
+ if (keyName === "key" || bindingName === "key") {
630
+ candidate = {
631
+ priority: 0,
632
+ expression: bindingName
633
+ };
634
+ } else {
635
+ candidate = {
636
+ priority: keyName === "data" ? 1 : 2,
637
+ expression: buildSlotScopeFallbackKeyExpression(bindingName)
638
+ };
639
+ }
640
+ }
641
+ }
642
+ if (candidate && (!best || candidate.priority < best.priority)) {
643
+ best = candidate;
644
+ }
645
+ }
646
+ return best;
647
+ }
648
+ function tryGetTemplateSlotScopeKeyExpression(scope) {
649
+ const trimmed = scope.trim();
650
+ if (!trimmed) {
651
+ return null;
652
+ }
653
+ if (isSimpleScopeIdentifier(trimmed)) {
654
+ return buildSlotScopeFallbackKeyExpression(trimmed);
655
+ }
656
+ try {
657
+ const parsed = parse(`(${trimmed}) => {}`, {
658
+ sourceType: "module",
659
+ plugins: ["typescript"]
660
+ });
661
+ const statement = parsed.program.body[0];
662
+ if (statement && isExpressionStatement(statement) && isArrowFunctionExpression(statement.expression)) {
663
+ return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
664
+ }
665
+ } catch {
666
+ }
667
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
668
+ const inner = trimmed.slice(1, -1).trim();
669
+ let cutIdx = -1;
670
+ const commaIdx = inner.indexOf(",");
671
+ const colonIdx = inner.indexOf(":");
672
+ if (commaIdx !== -1 && colonIdx !== -1) {
673
+ cutIdx = Math.min(commaIdx, colonIdx);
674
+ } else if (commaIdx !== -1) {
675
+ cutIdx = commaIdx;
676
+ } else if (colonIdx !== -1) {
677
+ cutIdx = colonIdx;
678
+ }
679
+ const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
680
+ if (first && isSimpleScopeIdentifier(first)) {
681
+ return buildSlotScopeFallbackKeyExpression(first);
682
+ }
683
+ }
684
+ return trimmed;
685
+ }
568
686
  function nodeHasToDirective(node) {
569
687
  const toDirective = findDirectiveByName(node, "bind", "to");
570
688
  if (toDirective?.exp) {
@@ -640,25 +758,7 @@ function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
640
758
  if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
641
759
  const scope = getTemplateSlotScope(parent);
642
760
  if (scope) {
643
- let key = scope.trim();
644
- if (key.startsWith("{") && key.endsWith("}")) {
645
- const inner = key.slice(1, -1).trim();
646
- let cutIdx = -1;
647
- const commaIdx = inner.indexOf(",");
648
- const colonIdx = inner.indexOf(":");
649
- if (commaIdx !== -1 && colonIdx !== -1) {
650
- cutIdx = Math.min(commaIdx, colonIdx);
651
- } else if (commaIdx !== -1) {
652
- cutIdx = commaIdx;
653
- } else if (colonIdx !== -1) {
654
- cutIdx = colonIdx;
655
- }
656
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
657
- if (first) {
658
- key = first;
659
- }
660
- }
661
- return key;
761
+ return tryGetTemplateSlotScopeKeyExpression(scope);
662
762
  }
663
763
  }
664
764
  parent = getParent(hierarchyMap2, parent);
@@ -1309,11 +1409,11 @@ function getStableClickHandlerNameFromExpression(exp) {
1309
1409
  return extractNameFromCallee(callee);
1310
1410
  }
1311
1411
  if (isAssignmentExpression(exp)) {
1312
- const left = exp.left;
1313
- if (isIdentifier(left))
1314
- return left.name;
1315
- if (isMemberExpression(left) || isOptionalMemberExpression(left))
1316
- return extractMemberPropertyName(left);
1412
+ const left = getAssignmentTargetNameFromBabel(exp.left);
1413
+ if (left) {
1414
+ const right = getStableAssignmentValueSuffixFromBabel(exp.right);
1415
+ return `Set${toPascalCase(left)}${right}`;
1416
+ }
1317
1417
  return "";
1318
1418
  }
1319
1419
  if (isOptionalMemberExpression(exp)) {
@@ -1388,6 +1488,71 @@ function extractMemberPropertyName(member) {
1388
1488
  }
1389
1489
  return "";
1390
1490
  }
1491
+ function getLastIdentifierFromMemberChainBabel(node) {
1492
+ if (!node) {
1493
+ return "";
1494
+ }
1495
+ if (isIdentifier(node)) {
1496
+ return node.name;
1497
+ }
1498
+ if (isMemberExpression(node) || isOptionalMemberExpression(node)) {
1499
+ if (!node.computed) {
1500
+ if (isIdentifier(node.property)) {
1501
+ return node.property.name;
1502
+ }
1503
+ } else if (isStringLiteral(node.property)) {
1504
+ return node.property.value;
1505
+ }
1506
+ }
1507
+ return "";
1508
+ }
1509
+ function getAssignmentTargetNameFromBabel(node) {
1510
+ if (!node) {
1511
+ return "";
1512
+ }
1513
+ if (isIdentifier(node)) {
1514
+ return node.name;
1515
+ }
1516
+ if (isMemberExpression(node) || isOptionalMemberExpression(node)) {
1517
+ if (!node.computed && isIdentifier(node.property) && node.property.name === "value") {
1518
+ return getLastIdentifierFromMemberChainBabel(node.object);
1519
+ }
1520
+ return getLastIdentifierFromMemberChainBabel(node);
1521
+ }
1522
+ return "";
1523
+ }
1524
+ function getStableAssignmentValueSuffixFromBabel(node) {
1525
+ if (!node) {
1526
+ return "";
1527
+ }
1528
+ if (isBooleanLiteral(node)) {
1529
+ return node.value ? "True" : "False";
1530
+ }
1531
+ if (isNumericLiteral(node)) {
1532
+ return `Value${String(node.value)}`;
1533
+ }
1534
+ if (isNullLiteral(node)) {
1535
+ return "Null";
1536
+ }
1537
+ if (isStringLiteral(node)) {
1538
+ const cleaned = (node.value ?? "").trim();
1539
+ return cleaned ? toPascalCase(cleaned.slice(0, 24)) : "";
1540
+ }
1541
+ if (isTemplateLiteral(node) && node.expressions.length === 0) {
1542
+ const value = node.quasis.map((q) => q.value?.cooked ?? "").join("").trim();
1543
+ return value ? toPascalCase(value.slice(0, 24)) : "";
1544
+ }
1545
+ if (isMemberExpression(node) || isOptionalMemberExpression(node)) {
1546
+ const name = getLastIdentifierFromMemberChainBabel(node);
1547
+ return name ? toPascalCase(name.slice(0, 24)) : "";
1548
+ }
1549
+ if (isIdentifier(node)) {
1550
+ const firstChar = node.name.charAt(0);
1551
+ const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1552
+ return isUpperAlpha ? toPascalCase(node.name.slice(0, 24)) : "";
1553
+ }
1554
+ return "";
1555
+ }
1391
1556
  function isCompilerGeneratedReferenceRoot(name) {
1392
1557
  return name.startsWith("_") || name.startsWith("$");
1393
1558
  }
@@ -1653,6 +1818,12 @@ function applyResolvedDataTestId(args) {
1653
1818
  const existingIdBehavior = args.existingIdBehavior ?? "preserve";
1654
1819
  const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
1655
1820
  const warn = args.warn;
1821
+ const getBestKeyAccessCandidates = (expr) => {
1822
+ if (!expr) {
1823
+ return [];
1824
+ }
1825
+ return expr.split("??").map((part) => part.trim()).filter(Boolean);
1826
+ };
1656
1827
  let dataTestId = args.preferredGeneratedValue;
1657
1828
  let fromExisting = false;
1658
1829
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
@@ -1675,6 +1846,7 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
1675
1846
  if (existingIdBehavior === "preserve") {
1676
1847
  if (existing.isDynamic) {
1677
1848
  if (existing.template) {
1849
+ const existingTemplate = existing.template;
1678
1850
  if ((existing.templateExpressionCount ?? 0) !== 1) {
1679
1851
  throw new Error(
1680
1852
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
@@ -1685,8 +1857,8 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1685
1857
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
1686
1858
  );
1687
1859
  }
1688
- const hasExact = args.bestKeyPlaceholder && existing.template.includes(args.bestKeyPlaceholder);
1689
- const hasVarAccess = args.bestKeyVariable && existing.template.includes(args.bestKeyVariable);
1860
+ const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
1861
+ const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
1690
1862
  if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
1691
1863
  throw new Error(
1692
1864
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
@@ -3199,6 +3371,10 @@ function escapeGitAttributesPattern(value) {
3199
3371
  }
3200
3372
  return output;
3201
3373
  }
3374
+ function pathUsesGeneratedHeuristic(filePath) {
3375
+ const normalized = path.normalize(filePath);
3376
+ return normalized.split(path.sep).includes("__generated__");
3377
+ }
3202
3378
  function buildManagedGitAttributesBlock(entries) {
3203
3379
  return [
3204
3380
  GENERATED_GITATTRIBUTES_BLOCK_START,
@@ -3246,6 +3422,9 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
3246
3422
  if (path.basename(resolvedFilePath) === ".gitattributes") {
3247
3423
  continue;
3248
3424
  }
3425
+ if (pathUsesGeneratedHeuristic(resolvedFilePath)) {
3426
+ continue;
3427
+ }
3249
3428
  const dir = path.dirname(resolvedFilePath);
3250
3429
  const entry = `${escapeGitAttributesPattern(path.basename(resolvedFilePath))} linguist-generated`;
3251
3430
  const entries = entriesByDir.get(dir) ?? /* @__PURE__ */ new Set();
@@ -3670,14 +3849,18 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3670
3849
  }
3671
3850
  return false;
3672
3851
  };
3852
+ const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
3853
+ const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? /* @__PURE__ */ new Set();
3673
3854
  const attachmentsForThisClass = customPomAttachments.filter((a) => {
3855
+ if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
3856
+ return false;
3674
3857
  const scope = a.attachTo ?? "views";
3675
3858
  const scopeOk = isView ? scope === "views" || scope === "both" : scope === "components" || scope === "both";
3676
3859
  if (!scopeOk)
3677
3860
  return false;
3678
3861
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));
3679
3862
  }).map((a) => ({
3680
- className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
3863
+ className: customPomClassIdentifierMap[a.className],
3681
3864
  propertyName: a.propertyName
3682
3865
  }));
3683
3866
  let content = "";
@@ -3717,7 +3900,7 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3717
3900
  content += `
3718
3901
  export class ${className} extends BasePage {
3719
3902
  `;
3720
- const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet) : [];
3903
+ const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
3721
3904
  const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
3722
3905
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
3723
3906
  content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
@@ -3875,6 +4058,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
3875
4058
  return customPomClassIdentifierMap2;
3876
4059
  };
3877
4060
  const customPomClassIdentifierMap = addCustomPomImports();
4061
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
3878
4062
  const referencedTargets = /* @__PURE__ */ new Set();
3879
4063
  for (const [, deps] of items) {
3880
4064
  for (const dt of deps.dataTestIdSet) {
@@ -4027,6 +4211,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4027
4211
  aggregated: true,
4028
4212
  customPomAttachments: options.customPomAttachments ?? [],
4029
4213
  customPomClassIdentifierMap,
4214
+ customPomAvailableClassIdentifiers,
4030
4215
  testIdAttribute: options.testIdAttribute,
4031
4216
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4032
4217
  routeMetaByComponent: options.routeMetaByComponent
@@ -4121,7 +4306,7 @@ function toPascalCaseLocal(str) {
4121
4306
  return preserveInternalCaps ? upperFirst(word) : upperFirst(word.toLowerCase());
4122
4307
  }).join("");
4123
4308
  }
4124
- function getWidgetInstancesForView(componentName, dataTestIdSet) {
4309
+ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassIdentifiers) {
4125
4310
  const out = [];
4126
4311
  const usedPropNames = /* @__PURE__ */ new Set();
4127
4312
  const ensureUnique = (base) => {
@@ -4152,6 +4337,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet) {
4152
4337
  } else {
4153
4338
  continue;
4154
4339
  }
4340
+ if (!availableClassIdentifiers.has(className))
4341
+ continue;
4155
4342
  const viewPrefix = `${componentName}-`;
4156
4343
  const descriptorRaw = stem.startsWith(viewPrefix) ? stem.slice(viewPrefix.length) : stem;
4157
4344
  const descriptorPascal = toPascalCaseLocal(descriptorRaw);
@@ -6156,7 +6343,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6156
6343
  const excludedComponents = injection.excludeComponents ?? [];
6157
6344
  const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
6158
6345
  const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
6159
- const outDir = (generationOptions?.outDir ?? "tests/playwright/generated").trim();
6346
+ const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
6160
6347
  const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
6161
6348
  const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
6162
6349
  const routerEntry = generationOptions?.router?.entry;