next-mw
Version:
A middleware manager for Next.js
1 lines • 13.3 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/matcher.ts"],"sourcesContent":["import {\n type NextRequest,\n type NextFetchEvent,\n NextResponse,\n} from 'next/server';\nimport { matchRequest, Matcher } from './matcher';\n\n/**\n * Type for a Next.js middleware result.\n */\nexport type NextMiddlewareResult =\n | NextResponse\n | Response\n | null\n | undefined\n | void;\n\n/**\n * Type for a Next.js middleware function.\n */\nexport type NextMiddleware = (\n req: NextRequest,\n ev: NextFetchEvent,\n) => NextMiddlewareResult | Promise<NextMiddlewareResult>;\n\n/**\n * Configuration option that uses only the \"matcher\" property.\n */\nexport type ConfigMatcher = {\n matcher: Matcher;\n include?: never;\n exclude?: never;\n};\n\n/**\n * Configuration option that uses \"include\" and/or \"exclude\".\n */\nexport type ConfigIncludeExclude = {\n matcher?: never;\n include?: Matcher;\n exclude?: Matcher;\n};\n\n/**\n * Configuration option type.\n */\nexport type Config = ConfigMatcher | ConfigIncludeExclude;\n\n/**\n * Middleware module type.\n * You can either use the \"matcher\" configuration\n * or the \"include\"/\"exclude\" configuration, but not both.\n */\nexport type MiddlewareModule = {\n middleware: NextMiddleware;\n config?: Config;\n};\n\n/**\n * Composes multiple middlewares into a single middleware.\n * Matching logic:\n * - If \"matcher\" is defined, it is used exclusively.\n * - Otherwise, if \"include\" is defined, the request must match at least one pattern.\n * - And if \"exclude\" is defined, the request must not match any of the patterns.\n *\n * @param modules - List of imported middleware modules.\n * @returns The composed middleware function.\n */\nexport function middlewares(...modules: MiddlewareModule[]): NextMiddleware {\n // Immediate validation of middleware configurations\n for (const module of modules) {\n if (module.config) {\n // Check that both 'matcher' and 'include/exclude' are not defined simultaneously\n if (\n module.config.matcher !== undefined &&\n (module.config.include !== undefined ||\n module.config.exclude !== undefined)\n ) {\n throw new Error(\n \"Cannot define both 'matcher' and 'include/exclude' in middleware config.\",\n );\n }\n }\n }\n\n // Return the composed middleware function\n return async function (req: NextRequest, ev: NextFetchEvent) {\n for (const module of modules) {\n if (module.config) {\n // Use 'matcher' if it is defined\n if (module.config.matcher !== undefined) {\n if (!matchRequest(req, module.config.matcher)) {\n continue;\n }\n } else {\n // Otherwise, check 'include' and 'exclude' conditions\n if (\n module.config.include !== undefined &&\n !matchRequest(req, module.config.include)\n ) {\n continue;\n }\n if (\n module.config.exclude !== undefined &&\n matchRequest(req, module.config.exclude)\n ) {\n continue;\n }\n }\n }\n const result = await module.middleware(req, ev);\n if (result) {\n return result;\n }\n }\n return NextResponse.next();\n };\n}\n","import type { NextRequest } from 'next/server';\nimport { match as pathMatch } from 'path-to-regexp';\n\n/**\n * Allowed literal types for matcher conditions.\n */\nexport type AllowedMatcherType = 'header' | 'query' | 'cookie';\n\n/**\n * Matcher element type.\n * It accepts either an object with a literal type (one of AllowedMatcherType)\n * or an object with a general string, so that object literals without assertions work.\n */\nexport type MatcherElement =\n | { type: AllowedMatcherType; key: string; value?: string }\n | { type: string; key: string; value?: string };\n\n/**\n * Interface for a matcher condition object.\n */\nexport interface MatcherCondition {\n source: string;\n regexp?: string;\n locale?: boolean;\n has?: MatcherElement[];\n missing?: MatcherElement[];\n}\n\n/**\n * Matcher can be a string, a MatcherCondition object, or an array of these.\n */\nexport type Matcher = string | MatcherCondition | (string | MatcherCondition)[];\n\n/**\n * Cache for compiled path-to-regexp matching functions.\n */\nconst matcherCache = new Map<string, ReturnType<typeof pathMatch>>();\n\n/**\n * Cache for compiled regular expressions.\n */\nconst regexCache = new Map<string, RegExp>();\n\n/**\n * Returns the matching function for a given pattern using caching.\n * @param pattern - The path pattern to compile.\n */\nfunction getMatcherFunction(pattern: string) {\n if (matcherCache.has(pattern)) {\n return matcherCache.get(pattern)!;\n }\n const fn = pathMatch(pattern, { decode: decodeURIComponent });\n matcherCache.set(pattern, fn);\n return fn;\n}\n\n/**\n * Returns the compiled regular expression for a given regex string using caching.\n * @param regexStr - The regular expression string.\n */\nfunction getRegex(regexStr: string): RegExp {\n if (regexCache.has(regexStr)) return regexCache.get(regexStr)!;\n const re = new RegExp(regexStr);\n regexCache.set(regexStr, re);\n return re;\n}\n\n/**\n * Checks if a given path matches the pattern.\n * @param path - The path to test.\n * @param pattern - The pattern to match.\n */\nfunction matchPath(path: string, pattern: string): boolean {\n const matcherFn = getMatcherFunction(pattern);\n return matcherFn(path) !== false;\n}\n\n/**\n * Checks if the \"has\" conditions are met in the request.\n * @param req - The NextRequest object.\n * @param conditions - Array of matcher element conditions.\n */\nfunction checkHasConditions(\n req: NextRequest,\n conditions: readonly MatcherElement[],\n): boolean {\n for (const condition of conditions) {\n switch (condition.type) {\n case 'header': {\n const headerValue = req.headers.get(condition.key);\n if (headerValue === null) return false;\n if (condition.value !== undefined && headerValue !== condition.value)\n return false;\n break;\n }\n case 'query': {\n const queryValue = req.nextUrl.searchParams.get(condition.key);\n if (queryValue === null) return false;\n if (condition.value !== undefined && queryValue !== condition.value)\n return false;\n break;\n }\n case 'cookie': {\n const cookie = req.cookies.get(condition.key);\n if (!cookie) return false;\n if (condition.value !== undefined && cookie.value !== condition.value)\n return false;\n break;\n }\n default:\n return false;\n }\n }\n return true;\n}\n\n/**\n * Checks if the \"missing\" conditions are met in the request.\n * Conditions are satisfied if the specified request element is absent\n * or does not match the provided value.\n * @param req - The NextRequest object.\n * @param conditions - Array of matcher element conditions.\n */\nfunction checkMissingConditions(\n req: NextRequest,\n conditions: readonly MatcherElement[],\n): boolean {\n for (const condition of conditions) {\n switch (condition.type) {\n case 'header': {\n const headerValue = req.headers.get(condition.key);\n if (headerValue !== null) {\n if (condition.value === undefined || headerValue === condition.value)\n return false;\n }\n break;\n }\n case 'query': {\n const queryValue = req.nextUrl.searchParams.get(condition.key);\n if (queryValue !== null) {\n if (condition.value === undefined || queryValue === condition.value)\n return false;\n }\n break;\n }\n case 'cookie': {\n const cookie = req.cookies.get(condition.key);\n if (cookie) {\n if (condition.value === undefined || cookie.value === condition.value)\n return false;\n }\n break;\n }\n default:\n return false;\n }\n }\n return true;\n}\n\n/**\n * Checks if the request matches the given matcher condition.\n * @param req - The NextRequest object.\n * @param condition - The matcher condition object.\n */\nexport function matchMatcherCondition(\n req: NextRequest,\n condition: MatcherCondition,\n): boolean {\n // Adjust the path based on locale configuration.\n let pathToMatch = req.nextUrl.pathname;\n if (condition.locale === false && req.nextUrl.locale) {\n const localePrefix = '/' + req.nextUrl.locale;\n if (pathToMatch.startsWith(localePrefix)) {\n pathToMatch = pathToMatch.slice(localePrefix.length) || '/';\n }\n }\n // Check the \"source\" pattern using path-to-regexp.\n if (!matchPath(pathToMatch, condition.source)) {\n return false;\n }\n // If a regexp is provided, test it against the path.\n if (condition.regexp) {\n const re = getRegex(condition.regexp);\n if (!re.test(pathToMatch)) {\n return false;\n }\n }\n // Check \"has\" conditions.\n if (condition.has && !checkHasConditions(req, condition.has)) {\n return false;\n }\n // Check \"missing\" conditions.\n if (condition.missing && !checkMissingConditions(req, condition.missing)) {\n return false;\n }\n return true;\n}\n\n/**\n * Checks if the request matches the provided matcher configuration.\n * @param req - The NextRequest object.\n * @param matcher - The matcher configuration.\n */\nexport function matchRequest(req: NextRequest, matcher?: Matcher): boolean {\n if (!matcher) return true;\n const matchers = Array.isArray(matcher) ? matcher : [matcher];\n\n // The request matches if it satisfies at least one of the matchers.\n for (const m of matchers) {\n if (typeof m === 'string') {\n if (matchPath(req.nextUrl.pathname, m)) {\n return true;\n }\n } else {\n if (matchMatcherCondition(req, m)) {\n return true;\n }\n }\n }\n return false;\n}\n"],"mappings":"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","names":[]}