UNPKG

eslint-plugin-next-route-params

Version:

eslint rule to enforce correct route parameters for Next.js

745 lines (741 loc) 27.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const testdata_1 = require("./testdata"); jest.mock("fs", () => { const actualFs = jest.requireActual("fs"); return { ...actualFs, readFileSync: jest.fn((filePath, options) => { if (filePath === path_1.default.join(process.cwd(), "package.json")) { return JSON.stringify({ dependencies: { next: "13.0.0" }, }); } return actualFs.readFileSync(filePath, options); }), existsSync: jest.fn((filePath) => { if (filePath === path_1.default.join(process.cwd(), "package.json")) { return true; } return actualFs.existsSync(filePath); }), }; }); const allCases = { valid: [ { name: "generateStaticParams allows returntype of one param 'id' is working", code: `export const generateStaticParams = () : {id: string }[] => []`, filename: path_1.default.join("src", "app", "movies", "[id]", "page.tsx"), }, { name: "The params of the default exported function has all file-based parameter keys ", code: `export default async function ReviewPage( parameters : { params: { reviewId: string; id: string }; }) { return null; } export const generateStaticParams = async function(parameters : {params : { id : string}}): Promise<{ id?: string; reviewId: string}[]> { return null; } `, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), }, { name: "The params of the default exported function has one of the file-based parameter keys ", code: "export default async function Page( parameters: { params: {id : string}}){return <div>Hallo</div>;}", filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), }, { name: "The default exported function has no function parameters", code: "export default function Page(){return <></>;}", filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), }, { name: "The default exported function's parameter are defined as a TypeReference within the file", code: ` type Params = { params : { ker: string }}; export default function Page(paramters : Params){return <></>;}`, filename: path_1.default.join("src", "app", "movies", "[id]", "[ker]", "page.tsx"), }, { code: ` export default function Page(parameters: { params: { id: string}, searchParams: { [key: string]: string | string[] | undefined } }) { return null;}`, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), }, { name: "A function which is later default exported is validated as the route compoenent", code: `export const generateMetadata = (parameters : { params : {id: string}}) => null; function Lawl(){ return null; } export default Lawl; `, filename: path_1.default.join("src", "app", "movies", "[id]", "page.tsx"), }, { name: "A function which is later default exported has the correct parameter type", code: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: Record<string, string | string[] | undefined> }) { return null; } export default Page; `, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: "searchParams also allows Array<string> instead of string[]", code: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: { [lol: string]: string | Array<string> | undefined } }) { return null; } export default Page; `, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: `The correct type of searchParams is validated`, code: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: { [key: string]: string | string[] | undefined } }) { return null; } export default Page; `, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: "A correct params type for generateMetadata is validated", code: `export function generateMetadata(parameters : { params: { id: string, other: string[] }, searchParams: { [key: string]: string | string[] | undefined } }) { return null; } `, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: "Returning a Promise from generateStaticParams is allowed", code: `export const generateStaticParams = async function (): Promise< { id?: string; other: string[] }[] > { return [{ other: ["hallo"] }]; };`, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: `searchParams is not validated if it is set to false in options`, code: ` function Page(parameters: { searchParams: { sort: "asc" | "desc" } }) { return null; } export default Page; `, options: [{ searchParams: false }], filename: path_1.default.join("src", "app", "page.tsx"), }, { name: `layout file can have children as Props`, code: ` function Layout(parameters: { children: React.ReactNode } ) { return null; } export default Layout; `, filename: path_1.default.join("src", "app", "layout.tsx"), }, ], invalid: [ { name: "Replace wrong parameter type of route-param in TSTypeLiteral", code: ` export default async function Page(parameters: { params: { id: number } }) { return <div>Hallo</div>; } `, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), output: ` export default async function Page(parameters: { params: { id: string } }) { return <div>Hallo</div>; } `, errors: [ { messageId: "issue:isWrongParameterType", data: { name: "id", type: "string" }, }, ], }, { name: "Replace unknown parameter in TSTypeLiteral", code: ` export default function Page(parameters: { params: { ids: number } }) { return <></>; } `, output: ` export default function Page(parameters: { params: { id: string; reviewId: string; } }) { return <></>; } `, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "ids" }, }, ], }, { name: "Replace type params and keep searchParams in TSTypeLiteral", code: ` export default function Page(parameters: { params: { idBang: string }, searchParams: { [key: string]: string | string[] | undefined } }) { return null;}`, output: ` export default function Page(parameters: { params: { id: string; reviewId: string; }, searchParams: { [key: string]: string | string[] | undefined } }) { return null;}`, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "idBang" }, }, ], }, { name: "Remove unknown Parameter and add known paramter in TSTypeLiteral (1/2)", code: ` export default function Page(paramters : { params : {iasd: string, id: string }}){return <></>;}`, output: ` export default function Page(paramters : { params : { id: string; reviewId: string; }}){return <></>;}`, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "iasd" }, }, ], }, { name: "Remove unknown Parameter and add known paramter in TSTypeLiteral (2/2)", code: `export default async function ReviewPage( parameters : { params: { reviewId: string, idasdfasdf: string }; }) { return null; }`, output: `export default async function ReviewPage( parameters : { params: { id: string; reviewId: string; }; }) { return null; }`, filename: path_1.default.join("src", "app", "movies", "[id]", "[reviewId]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "idasdfasdf" }, }, ], }, { name: "Remove unknown Parameters to empty Object (Record<string, never>)", code: ` export default async function ReviewPage({}: { params: { reviewId: string; idasdfasdf: string }; }) { return null; }`, output: ` export default async function ReviewPage({}: { params: Record<string, never>; }) { return null; }`, filename: path_1.default.join("src", "app", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "reviewId" }, }, ], }, { name: "Remove unknown Parameter", code: ` const Page = ({ params }: { params: { slugTypo: string } }) => { return <div>commentary</div>; } export default Page; `, output: ` const Page = ({ params }: { params: { slug: string; } }) => { return <div>commentary</div>; } export default Page; `, filename: path_1.default.join("src", "app", "movies", "[slug]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", data: { name: "slugTypo" }, }, ], }, { name: "Remove unknown Parameter in TSTypeLiteral (1/3)", code: `const Page = ({ params: { userId }, }: { params: { userId: string; lawl: string } }) => { return (<div>Hallo</div>) }; export default Page; `, errors: [ { messageId: "issue:unknown-parameter", data: { name: "lawl" }, }, ], filename: path_1.default.join("src", "app", "movies", "[userId]", "page.tsx"), output: `const Page = ({ params: { userId }, }: { params: { userId: string; } }) => { return (<div>Hallo</div>) }; export default Page; `, }, { name: "Remove unknown Parameter in TSTypeLiteral (2/3)", code: ` function Page ({ params: { userId }, }: { params: { userId: string; reviewId: string }; }) { return null; }; export default Page;`, filename: path_1.default.join("src", "app", "movies", "[userId]", "page.tsx"), output: ` function Page ({ params: { userId }, }: { params: { userId: string; }; }) { return null; }; export default Page;`, errors: [ { messageId: "issue:unknown-parameter", data: { name: "reviewId" }, }, ], }, { name: "Remove unknown Parameter in TSTypeLiteral (3/3)", code: ` const Page = ({ params: { userId }, } : { params: { userId: string; reviewId: string }}) => { return null; }; export default Page;`, filename: path_1.default.join("src", "app", "movies", "[userId]", "page.tsx"), output: ` const Page = ({ params: { userId }, } : { params: { userId: string; }}) => { return null; }; export default Page;`, errors: [ { messageId: "issue:unknown-parameter", data: { name: "reviewId" }, }, ], }, { name: "Replace param as dynamic Params with catchAll-Params in TSTypeReference", code: `function Page(parameters: { params: { id: string, other: string} }) { return null; } export default Page; `, output: `function Page(parameters: { params: { id: string, other: string[]} }) { return null; } export default Page; `, errors: [ { messageId: "issue:isWrongParameterType", data: { name: "other", type: "string[]" }, }, ], filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: "Replace type of param (TSTypeLiteral) in TSTypeReference with catchAll-param", code: `function Page(parameters: { params: { ids: number }}) { return null; } export default Page; `, output: `function Page(parameters: { params: { ids: string[] }}) { return null; } export default Page; `, errors: [ { messageId: "issue:isWrongParameterType", data: { name: "ids", type: "string[]" }, }, ], filename: path_1.default.join("src", "app", "movies", "[...ids]", "page.tsx"), }, { name: "Replace type of param (number) in TSTypeReference with catchAll-param", code: `function Page(parameters: { params: { id: null, other: {hallo : 2} }}) { return null; } export default Page; `, output: `function Page(parameters: { params: { id: string, other: string[] }}) { return null; } export default Page; `, filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "layout.tsx"), errors: [ { messageId: "issue:isWrongParameterType", data: { name: "id", type: "string" }, }, { messageId: "issue:isWrongParameterType", data: { name: "other", type: "string[]" }, }, ], }, { name: "Replace unknown parameter in TSTypeReference", code: ` function Page(parameters: { params: { otherTypo: string[] }}) { return null; } export default Page; `, output: ` function Page(parameters: { params: { other: string[]; }}) { return null; } export default Page; `, errors: [ { messageId: "issue:unknown-parameter", data: { name: "otherTypo" }, }, ], filename: path_1.default.join("src", "app", "[...other]", "layout.tsx"), }, { name: "Remove forbidden PropertyKey in TSTypeReference", code: ` type Params = { losterKey: any}; export default function Page(paramters : Params){return <></>;}`, output: ` type Params = { }; export default function Page(paramters : Params){return <></>;}`, errors: [ { messageId: "issue:forbiddenPropertyKey", data: { key: "losterKey" }, }, ], filename: path_1.default.join("src", "app", "movies", "[id]", "page.tsx"), }, { name: `The type of searchParams is validated`, code: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: { [key: string]: string | string[] | number } }) { return null; } export default Page; `, output: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: { [key: string]: string | string[] | undefined; } }) { return null; } export default Page; `, errors: [ { messageId: "issue:wrong-searchParams-type", }, ], filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: `The correct type of searchParams is validated`, code: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: number }) { return null; } export default Page;`, output: `function Page(parameters: { params: { id: string, other: string[]}, searchParams: { [key: string]: string | string[] | undefined; } }) { return null; } export default Page;`, errors: [ { messageId: "issue:wrong-searchParams-type", }, ], filename: path_1.default.join("src", "app", "movies", "[id]", "[...other]", "page.tsx"), }, { name: "The generateStaticParams function must return an array of objects with the correct type", code: `const generateStaticParams = function(parameters : {params : { id : string}}): { id: string; reviewId?: string} {return [{ id: "hallo"}]}`, output: `const generateStaticParams = function(parameters : {params : { id : string}}): { reviewId?: string; id: string; }[] {return [{ id: "hallo"}]}`, errors: [ { messageId: "issue:wrong-returntype", }, ], filename: path_1.default.join("src", "app", "movies", "[reviewId]", "[id]", "page.tsx"), }, { name: "generateStaticParams as function adds missing return type", code: `function generateStaticParams(){ return [] }`, output: `function generateStaticParams(): { id: string; }[] { return [] }`, filename: path_1.default.join("src", "app", "movies", "[id]", "page.tsx"), errors: [ { messageId: "issue:no-returntype", }, ], }, { name: "generateStaticParams as arrow function replaces keys of returntype of 'nick' with 'id' ", code: `export const generateStaticParams = () : { nick: string }[] => []`, output: `export const generateStaticParams = () : { id: string; }[] => []`, filename: path_1.default.join("src", "app", "movies", "[id]", "page.tsx"), errors: [ { messageId: "issue:unknown-parameter", }, ], }, { name: "generateStaticParams as arrow function misses current route param", code: `export const generateStaticParams = () : {optionalFirst: string, optionalSecond: string }[] => []`, output: `export const generateStaticParams = () : { optionalFirst?: string; optionalSecond?: string; id: string; }[] => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isNoOptionalParam", data: { name: "id" }, }, ], }, { name: "generateStaticParams as arrow function is not allowed to have current route param as optional", code: `export const generateStaticParams = () : {optionalFirst?: string, optionalSecond?: string, id?: string }[] => []`, output: `export const generateStaticParams = () : { optionalFirst?: string; optionalSecond?: string; id: string; }[] => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isNoOptionalParam", data: { name: "id" }, }, ], }, { name: "generateMetadata as arrow function is not allowed to have current route param as optional", code: `export const generateMetadata = ( props: { params : { id:number}} ) => []`, output: `export const generateMetadata = ( props: { params : { id:string}} ) => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isWrongParameterType", }, ], }, { name: "generateStaticParams as arrow function corrects output of wrond return type", code: `export const generateStaticParams = () : { id: number }[] => []`, output: `export const generateStaticParams = () : { id: string }[] => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isWrongParameterType", }, ], }, { name: "generateMetadata as arrow function adds missing return type", code: `export const generateStaticParams = () => []`, output: `export const generateStaticParams = () : { optionalFirst?: string; optionalSecond?: string; id: string; }[] => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:no-returntype", }, ], }, { name: "generateStaticParams as arrow function adds missing return type", code: `export const generateStaticParams = async () => []`, output: `export const generateStaticParams = async () : Promise<{ optionalFirst?: string; optionalSecond?: string; id: string; }[]> => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:no-returntype", }, ], }, { name: "async generateStaticParams as arrow function corrects wrong return type", code: `export const generateStaticParams = async (): Promise<{ id?: string }[]> => []`, output: `export const generateStaticParams = async (): Promise<{ optionalFirst?: string; optionalSecond?: string; id: string; }[]> => []`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isNoOptionalParam", }, ], }, { name: "async generateStaticParams as normal function corrects wrong return type", code: `export async function generateStaticParams(): Promise<{ id: number}[]> {return [];}`, output: `export async function generateStaticParams(): Promise<{ id: string}[]> {return [];}`, filename: path_1.default.join("src", "app", "movies", "[optionalFirst]", "[optionalSecond]", "[id]", "page.tsx"), errors: [ { messageId: "issue:isWrongParameterType", }, ], }, { name: "GET - RouteHandler correct params", code: ` export const GET = (req, { params }: { params: { wrong: string } }) => { return NextResponse.json({ hello: "world" }); }`, output: ` export const GET = (req, { params }: { params: { id: string; } }) => { return NextResponse.json({ hello: "world" }); }`, errors: [ { messageId: "issue:unknown-parameter", data: { name: "wrong" }, }, ], filename: path_1.default.join("src", "app", "[id]", "route.ts"), }, { name: "GET - RouteHandler corrects params", code: ` export function GET(req, { params }: { params: {wrong: string } }) { return NextResponse.json({ hello: "world" }); }`, output: ` export function GET(req, { params }: { params: { id: string; } }) { return NextResponse.json({ hello: "world" }); }`, errors: [ { messageId: "issue:unknown-parameter", data: { name: "wrong" }, }, ], filename: path_1.default.join("src", "app", "[id]", "route.ts"), }, ], }; const tester = (0, testdata_1.createTester)(allCases); describe("enforce-route-params next 13", () => { beforeAll(tester.setUp); afterAll(tester.cleanUp); tester.runTests(); });