eslint-plugin-next-route-params
Version:
eslint rule to enforce correct route parameters for Next.js
137 lines (136 loc) • 5.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateSearchParams = validateSearchParams;
const utils_1 = require("@typescript-eslint/utils");
const constants_1 = require("../constants");
const unwrapPromise_1 = require("./unwrapPromise");
const typeGenerator_1 = require("../types/typeGenerator");
function validateSearchParams(propsType, { ruleContext, customContext }) {
const searchParamsMember = propsType.members.find((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
member.key.name === constants_1.SEARCHPARAMS_PROP_NAME);
if (!searchParamsMember ||
!("typeAnnotation" in searchParamsMember) ||
!searchParamsMember.typeAnnotation) {
return;
}
function reportWrongSearchParamsTypeIssue(node, asyncRequestApi) {
ruleContext.report({
loc: searchParamsMember.loc,
messageId: "issue:wrong-searchParams-type",
data: { promiseOrEmpty: asyncRequestApi ? "a Promise" : "" },
fix: (fixer) => fixer.replaceTextRange(node.range, (0, typeGenerator_1.SEARCHPARAMS_TYPE_NODE_STRING)(asyncRequestApi)),
});
}
let typeToValidate;
if (customContext.asyncRequestAPI) {
const isPromise = (0, unwrapPromise_1.unwrapPromise)(searchParamsMember);
if (!isPromise.isPromise) {
reportWrongSearchParamsTypeIssue(searchParamsMember.typeAnnotation.typeAnnotation, true);
return;
}
else {
typeToValidate = isPromise.promiseType;
}
}
else {
typeToValidate = searchParamsMember.typeAnnotation.typeAnnotation;
}
//first check if the searchParams property is allowed
const isSearchParamsAllowedByFilenameType = customContext.allowedPropsForFileNameType?.includes(constants_1.SEARCHPARAMS_PROP_NAME);
if (!isSearchParamsAllowedByFilenameType) {
return; // no need to report the error because it is already done in validateFirstParameter
}
const isValid = validateSearchParamsStructure(typeToValidate);
if (!isValid) {
reportWrongSearchParamsTypeIssue(searchParamsMember.typeAnnotation.typeAnnotation, !!customContext.asyncRequestAPI);
}
}
function validateSearchParamsStructure(member) {
return (validateRecordTypeStructure(member) ||
validateIndexSignatureStructure(member));
}
function validateIndexSignatureStructure(member) {
if (!member || member.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral) {
return false;
}
const members = member.members;
if (members.length !== 1) {
return false;
}
const firstMember = members[0];
if (firstMember?.type !== utils_1.AST_NODE_TYPES.TSIndexSignature ||
firstMember.typeAnnotation?.type !== utils_1.AST_NODE_TYPES.TSTypeAnnotation ||
firstMember.typeAnnotation.typeAnnotation.type !==
utils_1.AST_NODE_TYPES.TSUnionType) {
return false;
}
const unionType = firstMember.typeAnnotation?.typeAnnotation;
const areAllowedSearchParamsTypes = extractTypes(unionType.types);
if (areAllowedSearchParamsTypes == null ||
Object.values(areAllowedSearchParamsTypes).includes(false)) {
return false;
}
return true;
}
/**
* required that input type is ```Record<string, string | string[] | undefined>```
*/
function validateRecordTypeStructure(typeNode) {
if (!typeNode || typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
return false;
}
// Ensure the base type is Record
if (typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier ||
typeNode.typeName.name !== "Record") {
return false;
}
const typeArguments = typeNode.typeArguments?.params;
if (!typeArguments || typeArguments.length !== 2) {
return false;
}
const [keyType, valueType] = typeArguments;
// Check if key type is string
if (keyType?.type !== utils_1.AST_NODE_TYPES.TSStringKeyword) {
return false;
}
// Check if value type is a union of string, string[], and undefined
if (valueType?.type !== utils_1.AST_NODE_TYPES.TSUnionType) {
return false;
}
const allowedValueTypes = extractTypes(valueType.types);
if (allowedValueTypes == null ||
Object.values(allowedValueTypes).includes(false)) {
return false;
}
return true;
}
function extractTypes(types) {
return types.reduce((acc, element) => {
if (acc == null) {
return acc;
}
if (element.type === utils_1.AST_NODE_TYPES.TSStringKeyword) {
acc.str = true;
return acc;
}
else if ((element.type === utils_1.AST_NODE_TYPES.TSArrayType &&
element.elementType.type === utils_1.AST_NODE_TYPES.TSStringKeyword) ||
(element.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
element.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
element.typeName.name === "Array" &&
element.typeArguments?.params.length === 1 &&
element.typeArguments.params[0]?.type ===
utils_1.AST_NODE_TYPES.TSStringKeyword)) {
acc.strArr = true;
return acc;
}
else if (element.type === utils_1.AST_NODE_TYPES.TSUndefinedKeyword) {
acc.undef = true;
return acc;
}
else {
return undefined;
}
}, { str: false, strArr: false, undef: false });
}