@immense/vue-pom-generator 1.0.60 → 1.0.62
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/README.md +9 -1
- package/RELEASE_NOTES.md +22 -28
- package/dist/accessibility-audit.d.ts +20 -0
- package/dist/accessibility-audit.d.ts.map +1 -0
- package/dist/compiler-metadata-utils.d.ts.map +1 -1
- package/dist/index.cjs +185 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +185 -60
- package/dist/index.mjs.map +1 -1
- package/dist/manifest-generator.d.ts +2 -0
- package/dist/manifest-generator.d.ts.map +1 -1
- package/dist/metadata-collector.d.ts +5 -1
- package/dist/metadata-collector.d.ts.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
- package/dist/plugin/resolved-generation-options.d.ts +1 -0
- package/dist/plugin/resolved-generation-options.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +7 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/vue-plugin.d.ts +1 -0
- package/dist/plugin/vue-plugin.d.ts.map +1 -1
- package/dist/tests/accessibility-audit.test.d.ts +2 -0
- package/dist/tests/accessibility-audit.test.d.ts.map +1 -0
- package/dist/tests/resolved-generation-options.test.d.ts +2 -0
- package/dist/tests/resolved-generation-options.test.d.ts.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -219,6 +219,7 @@ function resolveGenerationSupportOptions(options) {
|
|
|
219
219
|
nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
|
|
220
220
|
existingIdBehavior: options.existingIdBehavior ?? "error",
|
|
221
221
|
testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
|
|
222
|
+
accessibilityAudit: options.accessibilityAudit ?? false,
|
|
222
223
|
routerAwarePoms: options.routerAwarePoms ?? false,
|
|
223
224
|
routerEntry: options.routerEntry,
|
|
224
225
|
routerType: options.routerType ?? "vue-router",
|
|
@@ -1760,11 +1761,12 @@ function nodeHandlerAttributeInfo(node) {
|
|
|
1760
1761
|
return null;
|
|
1761
1762
|
}
|
|
1762
1763
|
const exp = handlerDirective.exp;
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1764
|
+
const transformedSource = getVueExpressionSource(exp, "content", "compiled");
|
|
1765
|
+
const authorSource = getVueExpressionSource(exp, "loc", "content", "compiled");
|
|
1766
|
+
if (!authorSource) {
|
|
1765
1767
|
return null;
|
|
1766
1768
|
}
|
|
1767
|
-
const mergeKey = `handler:expr:${
|
|
1769
|
+
const mergeKey = `handler:expr:${authorSource}`;
|
|
1768
1770
|
const expr = tryGetDirectiveBabelAst(handlerDirective, {
|
|
1769
1771
|
preferredViews: ["content", "compiled"],
|
|
1770
1772
|
plugins: ["typescript", "jsx"],
|
|
@@ -1772,9 +1774,7 @@ function nodeHandlerAttributeInfo(node) {
|
|
|
1772
1774
|
// That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
|
|
1773
1775
|
preferExistingAst: false
|
|
1774
1776
|
});
|
|
1775
|
-
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1777
|
+
const fallbackExpr = transformedSource !== authorSource ? tryParseBabelExpressionFromSource(authorSource, ["typescript", "jsx"]) : null;
|
|
1778
1778
|
const isNodeType2 = (node2, type) => {
|
|
1779
1779
|
return node2 !== null && node2.type === type;
|
|
1780
1780
|
};
|
|
@@ -2010,59 +2010,73 @@ function nodeHandlerAttributeInfo(node) {
|
|
|
2010
2010
|
const limited = parts.slice(0, 2);
|
|
2011
2011
|
return limited.map((p) => `${toPascalCase(p.key)}${p.value}`).join("");
|
|
2012
2012
|
};
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2013
|
+
const tryFromCallExpression = (call) => {
|
|
2014
|
+
const resolvedCall = isAwaitExpressionNode(call) ? call.argument : call;
|
|
2015
|
+
if (!isCallExpressionNode(resolvedCall)) {
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
const name = getLastIdentifierFromMemberChain(resolvedCall.callee);
|
|
2019
|
+
if (!name) {
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
const suffix = getStableSuffixFromCall(resolvedCall);
|
|
2023
|
+
const semanticNameHint2 = suffix ? `${toPascalCase(name)}${suffix}` : toPascalCase(name);
|
|
2024
|
+
return semanticNameHint2;
|
|
2025
|
+
};
|
|
2026
|
+
const resolveSemanticName = (candidateExpr) => {
|
|
2027
|
+
if (!candidateExpr) {
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
const direct = getLastIdentifierFromMemberChain(candidateExpr);
|
|
2031
|
+
if (direct) {
|
|
2032
|
+
return toPascalCase(direct);
|
|
2033
|
+
}
|
|
2034
|
+
if (isArrowFunctionExpressionNode(candidateExpr)) {
|
|
2035
|
+
const body = candidateExpr.body;
|
|
2036
|
+
const directCall = tryFromCallExpression(body);
|
|
2037
|
+
if (directCall) {
|
|
2038
|
+
return directCall;
|
|
2039
|
+
}
|
|
2040
|
+
if (isAssignmentExpressionNode(body)) {
|
|
2041
|
+
const lhs = getAssignmentTargetName(body.left);
|
|
2042
|
+
if (lhs) {
|
|
2043
|
+
const rhs = stableWordFromValue(body.right);
|
|
2044
|
+
return `Set${toPascalCase(lhs)}${rhs ?? ""}`;
|
|
2045
|
+
}
|
|
2027
2046
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const lhs = getAssignmentTargetName(body.left);
|
|
2038
|
-
if (lhs) {
|
|
2039
|
-
const rhs = stableWordFromValue(body.right);
|
|
2040
|
-
const semanticNameHint = `Set${toPascalCase(lhs)}${rhs ?? ""}`;
|
|
2041
|
-
return { semanticNameHint, mergeKey };
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
if (isBlockStatementNode(body)) {
|
|
2045
|
-
const stmts = body.body ?? [];
|
|
2046
|
-
if (stmts.length > 0) {
|
|
2047
|
-
const firstStmt = stmts[0];
|
|
2048
|
-
if (isReturnStatementNode(firstStmt)) {
|
|
2049
|
-
const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
|
|
2050
|
-
if (fromReturn) {
|
|
2051
|
-
return { semanticNameHint: fromReturn, mergeKey };
|
|
2047
|
+
if (isBlockStatementNode(body)) {
|
|
2048
|
+
const stmts = body.body ?? [];
|
|
2049
|
+
if (stmts.length > 0) {
|
|
2050
|
+
const firstStmt = stmts[0];
|
|
2051
|
+
if (isReturnStatementNode(firstStmt)) {
|
|
2052
|
+
const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
|
|
2053
|
+
if (fromReturn) {
|
|
2054
|
+
return fromReturn;
|
|
2055
|
+
}
|
|
2052
2056
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2057
|
+
if (isExpressionStatementNode(firstStmt)) {
|
|
2058
|
+
const fromExpr = tryFromCallExpression(firstStmt.expression ?? null);
|
|
2059
|
+
if (fromExpr) {
|
|
2060
|
+
return fromExpr;
|
|
2061
|
+
}
|
|
2058
2062
|
}
|
|
2059
2063
|
}
|
|
2060
2064
|
}
|
|
2065
|
+
const bodyName = getLastIdentifierFromMemberChain(body);
|
|
2066
|
+
if (bodyName) {
|
|
2067
|
+
return toPascalCase(bodyName);
|
|
2068
|
+
}
|
|
2061
2069
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2070
|
+
return null;
|
|
2071
|
+
};
|
|
2072
|
+
const isVueRefValueAccess = (candidate) => {
|
|
2073
|
+
return isMemberExpressionNode(candidate) && candidate.computed === false && isIdentifierNode2(candidate.property) && candidate.property.name === "value";
|
|
2074
|
+
};
|
|
2075
|
+
const transformedSemanticName = resolveSemanticName(expr);
|
|
2076
|
+
const fallbackSemanticName = resolveSemanticName(fallbackExpr);
|
|
2077
|
+
const semanticNameHint = fallbackSemanticName && transformedSource !== authorSource && isVueRefValueAccess(expr) ? fallbackSemanticName : transformedSemanticName ?? fallbackSemanticName;
|
|
2078
|
+
if (semanticNameHint) {
|
|
2079
|
+
return { semanticNameHint, mergeKey };
|
|
2066
2080
|
}
|
|
2067
2081
|
return null;
|
|
2068
2082
|
}
|
|
@@ -8173,6 +8187,59 @@ function createDevProcessorPlugin(options) {
|
|
|
8173
8187
|
}
|
|
8174
8188
|
};
|
|
8175
8189
|
}
|
|
8190
|
+
function supportsInlineTextAccessibleName(role) {
|
|
8191
|
+
return role === "button" || role === "radio";
|
|
8192
|
+
}
|
|
8193
|
+
function buildAccessibilityAudit(metadata, inferredRole) {
|
|
8194
|
+
if (!metadata || !inferredRole) {
|
|
8195
|
+
return void 0;
|
|
8196
|
+
}
|
|
8197
|
+
const role = normalizePomRoleLabel(inferredRole).toLowerCase();
|
|
8198
|
+
const dynamicProps = new Set(metadata.dynamicProps ?? []);
|
|
8199
|
+
const hasDynamicAccessibleNameSignal = dynamicProps.has("aria-label") || dynamicProps.has("title") || !!metadata.hasDynamicText;
|
|
8200
|
+
let accessibleNameSource;
|
|
8201
|
+
const reasons = [];
|
|
8202
|
+
if (metadata.staticAriaLabel) {
|
|
8203
|
+
accessibleNameSource = "aria-label";
|
|
8204
|
+
} else if (supportsInlineTextAccessibleName(role) && metadata.staticTextContent) {
|
|
8205
|
+
accessibleNameSource = "text";
|
|
8206
|
+
} else if (metadata.staticTitle) {
|
|
8207
|
+
accessibleNameSource = "title";
|
|
8208
|
+
} else if (hasDynamicAccessibleNameSignal) {
|
|
8209
|
+
accessibleNameSource = "dynamic";
|
|
8210
|
+
} else if (role === "input" || role === "select") {
|
|
8211
|
+
accessibleNameSource = "unknown";
|
|
8212
|
+
reasons.push("No inline accessible-name signal was found; the element may rely on external markup such as a separate <label>.");
|
|
8213
|
+
} else {
|
|
8214
|
+
accessibleNameSource = "missing";
|
|
8215
|
+
reasons.push("No compile-time accessible-name signal was found.");
|
|
8216
|
+
}
|
|
8217
|
+
return {
|
|
8218
|
+
needsReview: accessibleNameSource === "unknown" || accessibleNameSource === "missing",
|
|
8219
|
+
accessibleNameSource,
|
|
8220
|
+
reasons,
|
|
8221
|
+
...metadata.staticAriaLabel ? { staticAriaLabel: metadata.staticAriaLabel } : {},
|
|
8222
|
+
...metadata.staticRole ? { staticRole: metadata.staticRole } : {},
|
|
8223
|
+
...metadata.staticTitle ? { staticTitle: metadata.staticTitle } : {},
|
|
8224
|
+
...metadata.staticTextContent ? { staticTextContent: metadata.staticTextContent } : {}
|
|
8225
|
+
};
|
|
8226
|
+
}
|
|
8227
|
+
function collectAccessibilityReviewWarnings(manifest) {
|
|
8228
|
+
const warnings = [];
|
|
8229
|
+
for (const [componentName, component] of Object.entries(manifest)) {
|
|
8230
|
+
for (const entry of component.entries) {
|
|
8231
|
+
if (!entry.accessibility?.needsReview) {
|
|
8232
|
+
continue;
|
|
8233
|
+
}
|
|
8234
|
+
const entryLabel = entry.generatedPropertyName ?? entry.testId;
|
|
8235
|
+
const reasonText = entry.accessibility.reasons.join(" ");
|
|
8236
|
+
warnings.push(
|
|
8237
|
+
`[vue-pom-generator] Accessibility review suggested for ${componentName}.${entryLabel} (role=${entry.inferredRole ?? "unknown"}, testId=${JSON.stringify(entry.testId)}): ${reasonText}`
|
|
8238
|
+
);
|
|
8239
|
+
}
|
|
8240
|
+
}
|
|
8241
|
+
return warnings;
|
|
8242
|
+
}
|
|
8176
8243
|
function removeByKeySegment(value) {
|
|
8177
8244
|
const idx = value.indexOf("ByKey");
|
|
8178
8245
|
if (idx < 0) {
|
|
@@ -8231,6 +8298,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
|
|
|
8231
8298
|
...generatedActionName ? [generatedActionName] : [],
|
|
8232
8299
|
...extraActionNames.filter((name) => name !== generatedActionName)
|
|
8233
8300
|
]));
|
|
8301
|
+
const accessibility = buildAccessibilityAudit(metadata, pom?.nativeRole ?? null);
|
|
8234
8302
|
return {
|
|
8235
8303
|
testId,
|
|
8236
8304
|
selectorPatternKind: entry.selectorValue.patternKind,
|
|
@@ -8241,6 +8309,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
|
|
|
8241
8309
|
nativeRole: pom.nativeRole
|
|
8242
8310
|
}) : componentName,
|
|
8243
8311
|
inferredRole: pom?.nativeRole ?? null,
|
|
8312
|
+
...accessibility ? { accessibility } : {},
|
|
8244
8313
|
generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
|
|
8245
8314
|
generatedActionName,
|
|
8246
8315
|
generatedActionNames,
|
|
@@ -8516,6 +8585,33 @@ function parseDynamicProps(dynamicProps) {
|
|
|
8516
8585
|
}
|
|
8517
8586
|
return void 0;
|
|
8518
8587
|
}
|
|
8588
|
+
function findStaticAttributeValue(element, attributeName) {
|
|
8589
|
+
const attribute = element.props.find(
|
|
8590
|
+
(prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === attributeName
|
|
8591
|
+
);
|
|
8592
|
+
const value = attribute?.value?.content?.trim();
|
|
8593
|
+
return value ? value : void 0;
|
|
8594
|
+
}
|
|
8595
|
+
function collectStaticTextContent(children) {
|
|
8596
|
+
const parts = [];
|
|
8597
|
+
const visit = (nodes) => {
|
|
8598
|
+
for (const node of nodes) {
|
|
8599
|
+
if (node.type === NodeTypes.TEXT) {
|
|
8600
|
+
const text2 = node.content.replace(/\s+/g, " ").trim();
|
|
8601
|
+
if (text2) {
|
|
8602
|
+
parts.push(text2);
|
|
8603
|
+
}
|
|
8604
|
+
continue;
|
|
8605
|
+
}
|
|
8606
|
+
if (node.type === NodeTypes.ELEMENT) {
|
|
8607
|
+
visit(node.children);
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
};
|
|
8611
|
+
visit(children);
|
|
8612
|
+
const text = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
8613
|
+
return text || void 0;
|
|
8614
|
+
}
|
|
8519
8615
|
function tryCreateElementMetadata(args) {
|
|
8520
8616
|
const { element, semanticNameMap } = args;
|
|
8521
8617
|
const testIdAttribute = (args.testIdAttribute ?? "data-testid").trim() || "data-testid";
|
|
@@ -8539,13 +8635,9 @@ function tryCreateElementMetadata(args) {
|
|
|
8539
8635
|
dynamicPropsList = [content];
|
|
8540
8636
|
}
|
|
8541
8637
|
}
|
|
8542
|
-
const semanticName = semanticNameMap.get(testId);
|
|
8543
|
-
if (!semanticName) {
|
|
8544
|
-
return null;
|
|
8545
|
-
}
|
|
8546
8638
|
const metadata = {
|
|
8547
8639
|
testId,
|
|
8548
|
-
semanticName,
|
|
8640
|
+
semanticName: semanticNameMap.get(testId),
|
|
8549
8641
|
tag: element.tag,
|
|
8550
8642
|
tagType: element.tagType,
|
|
8551
8643
|
patchFlag,
|
|
@@ -8554,7 +8646,11 @@ function tryCreateElementMetadata(args) {
|
|
|
8554
8646
|
hasClickHandler: patchFlag ? Boolean(patchFlag & 32) : void 0,
|
|
8555
8647
|
hasDynamicClass: patchFlag ? Boolean(patchFlag & 2) : void 0,
|
|
8556
8648
|
hasDynamicStyle: patchFlag ? Boolean(patchFlag & 4) : void 0,
|
|
8557
|
-
hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0
|
|
8649
|
+
hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0,
|
|
8650
|
+
staticAriaLabel: findStaticAttributeValue(element, "aria-label"),
|
|
8651
|
+
staticRole: findStaticAttributeValue(element, "role"),
|
|
8652
|
+
staticTitle: findStaticAttributeValue(element, "title"),
|
|
8653
|
+
staticTextContent: collectStaticTextContent(element.children)
|
|
8558
8654
|
};
|
|
8559
8655
|
return metadata;
|
|
8560
8656
|
}
|
|
@@ -8606,6 +8702,7 @@ function extractMetadataAfterTransform(ast, componentName, elementMetadata, sema
|
|
|
8606
8702
|
if (componentMetadata.size > 0) {
|
|
8607
8703
|
elementMetadata.set(componentName, componentMetadata);
|
|
8608
8704
|
}
|
|
8705
|
+
return componentMetadata;
|
|
8609
8706
|
}
|
|
8610
8707
|
function createVuePluginWithTestIds(options) {
|
|
8611
8708
|
const {
|
|
@@ -8620,11 +8717,13 @@ function createVuePluginWithTestIds(options) {
|
|
|
8620
8717
|
excludedComponents,
|
|
8621
8718
|
getViewsDirAbs,
|
|
8622
8719
|
testIdAttribute,
|
|
8720
|
+
accessibilityAudit,
|
|
8623
8721
|
loggerRef,
|
|
8624
8722
|
getSourceDirs,
|
|
8625
8723
|
getWrapperSearchRoots,
|
|
8626
8724
|
getProjectRoot
|
|
8627
8725
|
} = options;
|
|
8726
|
+
const lastAccessibilityWarningSignatureByComponent = /* @__PURE__ */ new Map();
|
|
8628
8727
|
const getComponentNameFromPath = (filename) => {
|
|
8629
8728
|
return resolveComponentNameFromPath({
|
|
8630
8729
|
filename,
|
|
@@ -8697,13 +8796,37 @@ function createVuePluginWithTestIds(options) {
|
|
|
8697
8796
|
)
|
|
8698
8797
|
);
|
|
8699
8798
|
return () => {
|
|
8700
|
-
extractMetadataAfterTransform(
|
|
8799
|
+
const componentMetadata = extractMetadataAfterTransform(
|
|
8701
8800
|
node,
|
|
8702
8801
|
componentName,
|
|
8703
8802
|
elementMetadata,
|
|
8704
8803
|
semanticNameMap,
|
|
8705
8804
|
testIdAttribute
|
|
8706
8805
|
);
|
|
8806
|
+
if (!accessibilityAudit) {
|
|
8807
|
+
return;
|
|
8808
|
+
}
|
|
8809
|
+
const dependencies = componentHierarchyMap.get(componentName);
|
|
8810
|
+
if (!dependencies || componentMetadata.size === 0) {
|
|
8811
|
+
return;
|
|
8812
|
+
}
|
|
8813
|
+
const manifest = buildPomManifest(
|
|
8814
|
+
/* @__PURE__ */ new Map([[componentName, dependencies]]),
|
|
8815
|
+
/* @__PURE__ */ new Map([[componentName, componentMetadata]])
|
|
8816
|
+
);
|
|
8817
|
+
const warnings = collectAccessibilityReviewWarnings(manifest);
|
|
8818
|
+
const signature = warnings.join("\n");
|
|
8819
|
+
if (!signature) {
|
|
8820
|
+
lastAccessibilityWarningSignatureByComponent.delete(componentName);
|
|
8821
|
+
return;
|
|
8822
|
+
}
|
|
8823
|
+
if (lastAccessibilityWarningSignatureByComponent.get(componentName) === signature) {
|
|
8824
|
+
return;
|
|
8825
|
+
}
|
|
8826
|
+
lastAccessibilityWarningSignatureByComponent.set(componentName, signature);
|
|
8827
|
+
for (const warning of warnings) {
|
|
8828
|
+
loggerRef.current.warn(warning);
|
|
8829
|
+
}
|
|
8707
8830
|
};
|
|
8708
8831
|
}
|
|
8709
8832
|
let transform = perFileTransform.get(componentName);
|
|
@@ -9068,6 +9191,7 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
9068
9191
|
nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
|
|
9069
9192
|
existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
|
|
9070
9193
|
testIdAttribute,
|
|
9194
|
+
accessibilityAudit: generationOptions?.accessibilityAudit,
|
|
9071
9195
|
routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
|
|
9072
9196
|
routerEntry,
|
|
9073
9197
|
routerType,
|
|
@@ -9161,6 +9285,7 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
9161
9285
|
excludedComponents,
|
|
9162
9286
|
getViewsDirAbs,
|
|
9163
9287
|
testIdAttribute,
|
|
9288
|
+
accessibilityAudit: resolvedGenerationOptions.accessibilityAudit,
|
|
9164
9289
|
loggerRef,
|
|
9165
9290
|
getSourceDirs,
|
|
9166
9291
|
getWrapperSearchRoots: getWrapperSearchRootsAbs,
|