eslint-plugin-next-route-params
Version:
eslint rule to enforce correct route parameters for Next.js
178 lines (177 loc) • 8.24 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils/utils");
const utils_2 = require("@typescript-eslint/utils");
const fs_1 = require("../utils/fs");
const validateFunction_1 = require("../utils/validation/validateGenerateStaticParams/validateFunction");
const constants_1 = require("../utils/constants");
const createRule = utils_2.ESLintUtils.RuleCreator(() => `https://www.paulhe.de/blog/next-route-params-eslint-rule`);
const GENERATE_STATIC_PARAMS_FUNCTION_NAME = "generateStaticParams";
const enforceRouteParamsRule = createRule({
name: "enforce-route-params",
meta: {
docs: {
description: "enforce correct route parameters built by Next.js' file based routes",
},
type: "problem",
messages: {
"issue:isWrongParameterType": "{{ name }} must be of type {{ type }}",
"issue:isNoLiteral": "Consider using an explicit type annotation",
"issue:unknown-parameter": "The param {{ name }} does not exist in the corresponding route path of this file",
"issue:forbiddenPropertyKey": "The property {{ key }} is forbidden",
"issue:wrong-searchParams-type": "searchParams must be {{ promiseOrEmpty }} of type { [key: string]: string | string[] | undefined }",
"issue:no-returntype": "The function must specify a returntype",
"issue:wrong-returntype": "The function must specify a correct returntype",
"issue:isNoOptionalParam": "The param {{ name }} must not be optional",
"issue:asyncRequestApi-params": "params must be a Promise",
},
schema: [
{
type: "object",
properties: {
searchParams: {
type: "boolean",
enum: [true, false],
default: true,
description: "If true, also strictly validates searchParams and enforces that the searchParams parameter is of type { [key: string]: string | string[] | undefined }",
},
},
additionalProperties: false,
},
],
fixable: "code",
hasSuggestions: false,
},
defaultOptions: [
{
searchParams: true,
},
],
create: (ruleContext, options) => {
const fileInfo = (0, fs_1.getFileInfo)(ruleContext.filename);
if (!fileInfo.inInAppRouterFolder ||
!fileInfo.isAppRouterFile ||
fileInfo.asyncRequestAPI == null) {
return {};
}
let nameOfDefaultExport = null;
const context = {
ruleContext,
customContext: {
...fileInfo,
},
};
const registeredRouteHandlerFunctions = [];
return {
FunctionDeclaration(node) {
if (node.id?.name === GENERATE_STATIC_PARAMS_FUNCTION_NAME) {
(0, validateFunction_1.validateGenerateStaticParamsFunction)(node, context);
}
else if (node.id?.name &&
constants_1.METADATA_FUNCTION_NAMES.includes(node.id?.name) &&
node.params[0] != null) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.params[0],
});
}
else if (node.id?.name &&
registeredRouteHandlerFunctions.includes(node.id?.name) &&
node.params[1] != null) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.params[1],
});
}
},
// Handle variable declarations with arrow functions
VariableDeclarator(node) {
if ((node.init?.type === utils_2.AST_NODE_TYPES.ArrowFunctionExpression ||
node.init?.type === utils_2.AST_NODE_TYPES.FunctionExpression) &&
node.id.type === utils_2.AST_NODE_TYPES.Identifier) {
if (node.id.name === GENERATE_STATIC_PARAMS_FUNCTION_NAME) {
(0, validateFunction_1.validateGenerateStaticParamsFunction)(node.init, context);
}
else if (constants_1.METADATA_FUNCTION_NAMES.includes(node.id?.name) &&
node.init.params[0] != null) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.init.params[0],
});
}
else if (constants_1.ROUTE_HANDLERS_FUNCTION_NAMES.includes(node.id?.name) &&
node.init.params[1] != null) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.init.params[1],
});
}
}
},
ExportNamedDeclaration(node) {
if (node.declaration?.type === utils_2.AST_NODE_TYPES.FunctionDeclaration &&
node.declaration.id?.name != null &&
constants_1.ROUTE_HANDLERS_FUNCTION_NAMES.includes(node.declaration.id?.name) &&
node.declaration.params[1] != null) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.declaration.params[1],
});
}
},
ExportDefaultDeclaration(node) {
if (node.declaration.type === utils_2.AST_NODE_TYPES.Identifier) {
nameOfDefaultExport = node.declaration.name;
return;
}
// it is not a default function export
if (!(node.declaration.type === utils_2.AST_NODE_TYPES.FunctionDeclaration) ||
!node.declaration.params[0]) {
return;
}
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.declaration.params[0],
});
},
"Program:exit"() {
if (nameOfDefaultExport) {
const variable = ruleContext.sourceCode.scopeManager?.variables?.find((variable) => variable.name === nameOfDefaultExport);
if (!variable) {
return;
}
const node = variable.defs[0]?.node;
// Perform your custom logic on the declaration node here
if (node?.type === utils_2.AST_NODE_TYPES.VariableDeclarator && node.init) {
const initNode = node.init;
if ((initNode.type === utils_2.AST_NODE_TYPES.ArrowFunctionExpression ||
initNode.type === utils_2.AST_NODE_TYPES.FunctionExpression) &&
initNode.params[0]) {
// Add your custom logic for the function node here
(0, utils_1.handleFunctionParameters)({
context,
options,
props: initNode.params[0],
});
}
}
else if (node?.type === utils_2.AST_NODE_TYPES.FunctionDeclaration &&
node.params[0]) {
(0, utils_1.handleFunctionParameters)({
context,
options,
props: node.params[0],
});
}
}
},
};
},
});
exports.default = enforceRouteParamsRule;