This commit is contained in:
Kismet Hasanaj
2026-05-02 20:07:02 +02:00
parent ce8672e283
commit 34dc9aec52
9428 changed files with 1733330 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import { jsx as _jsx } from "react/jsx-runtime";
import React, { useMemo, useRef } from 'react';
import { PathnameContext } from '../hooks-client-context.shared-runtime';
import { isDynamicRoute } from './utils';
import { asPathToSearchParams } from './utils/as-path-to-search-params';
import { getRouteRegex } from './utils/route-regex';
/** It adapts a Pages Router (`NextRouter`) to the App Router Instance. */ export function adaptForAppRouterInstance(pagesRouter) {
return {
back () {
pagesRouter.back();
},
forward () {
pagesRouter.forward();
},
refresh () {
pagesRouter.reload();
},
hmrRefresh () {},
push (href, { scroll } = {}) {
void pagesRouter.push(href, undefined, {
scroll
});
},
replace (href, { scroll } = {}) {
void pagesRouter.replace(href, undefined, {
scroll
});
},
prefetch (href) {
void pagesRouter.prefetch(href);
}
};
}
/**
* adaptForSearchParams transforms the ParsedURLQuery into URLSearchParams.
*
* @param router the router that contains the query.
* @returns the search params in the URLSearchParams format
*/ export function adaptForSearchParams(router) {
if (!router.isReady || !router.query) {
return new URLSearchParams();
}
return asPathToSearchParams(router.asPath);
}
export function adaptForPathParams(router) {
if (!router.isReady || !router.query) {
return null;
}
const pathParams = {};
const routeRegex = getRouteRegex(router.pathname);
const keys = Object.keys(routeRegex.groups);
for (const key of keys){
pathParams[key] = router.query[key];
}
return pathParams;
}
export function PathnameContextProviderAdapter({ children, router, ...props }) {
const ref = useRef(props.isAutoExport);
const value = useMemo(()=>{
// isAutoExport is only ever `true` on the first render from the server,
// so reset it to `false` after we read it for the first time as `true`. If
// we don't use the value, then we don't need it.
const isAutoExport = ref.current;
if (isAutoExport) {
ref.current = false;
}
// When the route is a dynamic route, we need to do more processing to
// determine if we need to stop showing the pathname.
if (isDynamicRoute(router.pathname)) {
// When the router is rendering the fallback page, it can't possibly know
// the path, so return `null` here. Read more about fallback pages over
// at:
// https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-pages
if (router.isFallback) {
return null;
}
// When `isAutoExport` is true, meaning this is a page page has been
// automatically statically optimized, and the router is not ready, then
// we can't know the pathname yet. Read more about automatic static
// optimization at:
// https://nextjs.org/docs/advanced-features/automatic-static-optimization
if (isAutoExport && !router.isReady) {
return null;
}
}
// The `router.asPath` contains the pathname seen by the browser (including
// any query strings), so it should have that stripped. Read more about the
// `asPath` option over at:
// https://nextjs.org/docs/api-reference/next/router#router-object
let url;
try {
url = new URL(router.asPath, 'http://f');
} catch (_) {
// fallback to / for invalid asPath values e.g. //
return '/';
}
return url.pathname;
}, [
router.asPath,
router.isFallback,
router.isReady,
router.pathname
]);
return /*#__PURE__*/ _jsx(PathnameContext.Provider, {
value: value,
children: children
});
}
//# sourceMappingURL=adapters.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+92
View File
@@ -0,0 +1,92 @@
import { InvariantError } from '../../invariant-error';
import { getSegmentParam } from '../utils/get-segment-param';
import { INTERCEPTION_ROUTE_MARKERS } from '../utils/interception-routes';
export function parseAppRouteSegment(segment) {
if (segment === '') {
return null;
}
// Check if the segment starts with an interception marker
const interceptionMarker = INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m));
const param = getSegmentParam(segment);
if (param) {
return {
type: 'dynamic',
name: segment,
param,
interceptionMarker
};
} else if (segment.startsWith('(') && segment.endsWith(')')) {
return {
type: 'route-group',
name: segment,
interceptionMarker
};
} else if (segment.startsWith('@')) {
return {
type: 'parallel-route',
name: segment,
interceptionMarker
};
} else {
return {
type: 'static',
name: segment,
interceptionMarker
};
}
}
export function isNormalizedAppRoute(route) {
return route.normalized;
}
export function isInterceptionAppRoute(route) {
return route.interceptionMarker !== undefined && route.interceptingRoute !== undefined && route.interceptedRoute !== undefined;
}
export function parseAppRoute(pathname, normalized) {
const pathnameSegments = pathname.split('/').filter(Boolean);
// Build segments array with static and dynamic segments
const segments = [];
// Parse if this is an interception route.
let interceptionMarker;
let interceptingRoute;
let interceptedRoute;
for (const segment of pathnameSegments){
// Parse the segment into an AppSegment.
const appSegment = parseAppRouteSegment(segment);
if (!appSegment) {
continue;
}
if (normalized && (appSegment.type === 'route-group' || appSegment.type === 'parallel-route')) {
throw Object.defineProperty(new InvariantError(`${pathname} is being parsed as a normalized route, but it has a route group or parallel route segment.`), "__NEXT_ERROR_CODE", {
value: "E923",
enumerable: false,
configurable: true
});
}
segments.push(appSegment);
if (appSegment.interceptionMarker) {
const parts = pathname.split(appSegment.interceptionMarker);
if (parts.length !== 2) {
throw Object.defineProperty(new Error(`Invalid interception route: ${pathname}`), "__NEXT_ERROR_CODE", {
value: "E924",
enumerable: false,
configurable: true
});
}
interceptingRoute = normalized ? parseAppRoute(parts[0], true) : parseAppRoute(parts[0], false);
interceptedRoute = normalized ? parseAppRoute(parts[1], true) : parseAppRoute(parts[1], false);
interceptionMarker = appSegment.interceptionMarker;
}
}
const dynamicSegments = segments.filter((segment)=>segment.type === 'dynamic');
return {
normalized,
pathname,
segments,
dynamicSegments,
interceptionMarker,
interceptingRoute,
interceptedRoute
};
}
//# sourceMappingURL=app.js.map
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
import { addPathPrefix } from './add-path-prefix';
import { pathHasPrefix } from './path-has-prefix';
/**
* For a given path and a locale, if the locale is given, it will prefix the
* locale. The path shouldn't be an API path. If a default locale is given the
* prefix will be omitted if the locale is already the default locale.
*/ export function addLocale(path, locale, defaultLocale, ignorePrefix) {
// If no locale was given or the locale is the default locale, we don't need
// to prefix the path.
if (!locale || locale === defaultLocale) return path;
const lower = path.toLowerCase();
// If the path is an API path or the path already has the locale prefix, we
// don't need to prefix the path.
if (!ignorePrefix) {
if (pathHasPrefix(lower, '/api')) return path;
if (pathHasPrefix(lower, `/${locale.toLowerCase()}`)) return path;
}
// Add the locale prefix to the path.
return addPathPrefix(path, `/${locale}`);
}
//# sourceMappingURL=add-locale.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/add-locale.ts"],"sourcesContent":["import { addPathPrefix } from './add-path-prefix'\nimport { pathHasPrefix } from './path-has-prefix'\n\n/**\n * For a given path and a locale, if the locale is given, it will prefix the\n * locale. The path shouldn't be an API path. If a default locale is given the\n * prefix will be omitted if the locale is already the default locale.\n */\nexport function addLocale(\n path: string,\n locale?: string | false,\n defaultLocale?: string,\n ignorePrefix?: boolean\n) {\n // If no locale was given or the locale is the default locale, we don't need\n // to prefix the path.\n if (!locale || locale === defaultLocale) return path\n\n const lower = path.toLowerCase()\n\n // If the path is an API path or the path already has the locale prefix, we\n // don't need to prefix the path.\n if (!ignorePrefix) {\n if (pathHasPrefix(lower, '/api')) return path\n if (pathHasPrefix(lower, `/${locale.toLowerCase()}`)) return path\n }\n\n // Add the locale prefix to the path.\n return addPathPrefix(path, `/${locale}`)\n}\n"],"names":["addPathPrefix","pathHasPrefix","addLocale","path","locale","defaultLocale","ignorePrefix","lower","toLowerCase"],"mappings":"AAAA,SAASA,aAAa,QAAQ,oBAAmB;AACjD,SAASC,aAAa,QAAQ,oBAAmB;AAEjD;;;;CAIC,GACD,OAAO,SAASC,UACdC,IAAY,EACZC,MAAuB,EACvBC,aAAsB,EACtBC,YAAsB;IAEtB,4EAA4E;IAC5E,sBAAsB;IACtB,IAAI,CAACF,UAAUA,WAAWC,eAAe,OAAOF;IAEhD,MAAMI,QAAQJ,KAAKK,WAAW;IAE9B,2EAA2E;IAC3E,iCAAiC;IACjC,IAAI,CAACF,cAAc;QACjB,IAAIL,cAAcM,OAAO,SAAS,OAAOJ;QACzC,IAAIF,cAAcM,OAAO,CAAC,CAAC,EAAEH,OAAOI,WAAW,IAAI,GAAG,OAAOL;IAC/D;IAEA,qCAAqC;IACrC,OAAOH,cAAcG,MAAM,CAAC,CAAC,EAAEC,QAAQ;AACzC","ignoreList":[0]}
+13
View File
@@ -0,0 +1,13 @@
import { parsePath } from './parse-path';
/**
* Adds the provided prefix to the given path. It first ensures that the path
* is indeed starting with a slash.
*/ export function addPathPrefix(path, prefix) {
if (!path.startsWith('/') || !prefix) {
return path;
}
const { pathname, query, hash } = parsePath(path);
return `${prefix}${pathname}${query}${hash}`;
}
//# sourceMappingURL=add-path-prefix.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/add-path-prefix.ts"],"sourcesContent":["import { parsePath } from './parse-path'\n\n/**\n * Adds the provided prefix to the given path. It first ensures that the path\n * is indeed starting with a slash.\n */\nexport function addPathPrefix(path: string, prefix?: string) {\n if (!path.startsWith('/') || !prefix) {\n return path\n }\n\n const { pathname, query, hash } = parsePath(path)\n return `${prefix}${pathname}${query}${hash}`\n}\n"],"names":["parsePath","addPathPrefix","path","prefix","startsWith","pathname","query","hash"],"mappings":"AAAA,SAASA,SAAS,QAAQ,eAAc;AAExC;;;CAGC,GACD,OAAO,SAASC,cAAcC,IAAY,EAAEC,MAAe;IACzD,IAAI,CAACD,KAAKE,UAAU,CAAC,QAAQ,CAACD,QAAQ;QACpC,OAAOD;IACT;IAEA,MAAM,EAAEG,QAAQ,EAAEC,KAAK,EAAEC,IAAI,EAAE,GAAGP,UAAUE;IAC5C,OAAO,GAAGC,SAASE,WAAWC,QAAQC,MAAM;AAC9C","ignoreList":[0]}
+14
View File
@@ -0,0 +1,14 @@
import { parsePath } from './parse-path';
/**
* Similarly to `addPathPrefix`, this function adds a suffix at the end on the
* provided path. It also works only for paths ensuring the argument starts
* with a slash.
*/ export function addPathSuffix(path, suffix) {
if (!path.startsWith('/') || !suffix) {
return path;
}
const { pathname, query, hash } = parsePath(path);
return `${pathname}${suffix}${query}${hash}`;
}
//# sourceMappingURL=add-path-suffix.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/add-path-suffix.ts"],"sourcesContent":["import { parsePath } from './parse-path'\n\n/**\n * Similarly to `addPathPrefix`, this function adds a suffix at the end on the\n * provided path. It also works only for paths ensuring the argument starts\n * with a slash.\n */\nexport function addPathSuffix(path: string, suffix?: string) {\n if (!path.startsWith('/') || !suffix) {\n return path\n }\n\n const { pathname, query, hash } = parsePath(path)\n return `${pathname}${suffix}${query}${hash}`\n}\n"],"names":["parsePath","addPathSuffix","path","suffix","startsWith","pathname","query","hash"],"mappings":"AAAA,SAASA,SAAS,QAAQ,eAAc;AAExC;;;;CAIC,GACD,OAAO,SAASC,cAAcC,IAAY,EAAEC,MAAe;IACzD,IAAI,CAACD,KAAKE,UAAU,CAAC,QAAQ,CAACD,QAAQ;QACpC,OAAOD;IACT;IAEA,MAAM,EAAEG,QAAQ,EAAEC,KAAK,EAAEC,IAAI,EAAE,GAAGP,UAAUE;IAC5C,OAAO,GAAGG,WAAWF,SAASG,QAAQC,MAAM;AAC9C","ignoreList":[0]}
+66
View File
@@ -0,0 +1,66 @@
import { ensureLeadingSlash } from '../../page-path/ensure-leading-slash';
import { isGroupSegment } from '../../segment';
/**
* Normalizes an app route so it represents the actual request path. Essentially
* performing the following transformations:
*
* - `/(dashboard)/user/[id]/page` to `/user/[id]`
* - `/(dashboard)/account/page` to `/account`
* - `/user/[id]/page` to `/user/[id]`
* - `/account/page` to `/account`
* - `/page` to `/`
* - `/(dashboard)/user/[id]/route` to `/user/[id]`
* - `/(dashboard)/account/route` to `/account`
* - `/user/[id]/route` to `/user/[id]`
* - `/account/route` to `/account`
* - `/route` to `/`
* - `/` to `/`
*
* @param route the app route to normalize
* @returns the normalized pathname
*/ export function normalizeAppPath(route) {
return ensureLeadingSlash(route.split('/').reduce((pathname, segment, index, segments)=>{
// Empty segments are ignored.
if (!segment) {
return pathname;
}
// Groups are ignored.
if (isGroupSegment(segment)) {
return pathname;
}
// Parallel segments are ignored.
if (segment[0] === '@') {
return pathname;
}
// The last segment (if it's a leaf) should be ignored.
if ((segment === 'page' || segment === 'route') && index === segments.length - 1) {
return pathname;
}
return `${pathname}/${segment}`;
}, ''));
}
/**
* Comparator for sorting app paths so that parallel slot paths (containing
* `/@`) come before the children/root page path. This ensures the last item
* is always the children page, which is what `renderPageComponent` reads via
* `appPaths[appPaths.length - 1]`.
*
* Without this, route group prefixes like `(group)` (char code 0x28) sort
* before `@` (0x40), causing the children page to sort first instead of last
* and leading to a manifest mismatch / 404 in webpack dev mode.
*/ export function compareAppPaths(a, b) {
const aHasSlot = a.includes('/@');
const bHasSlot = b.includes('/@');
if (aHasSlot && !bHasSlot) return -1;
if (!aHasSlot && bHasSlot) return 1;
return a.localeCompare(b);
}
/**
* Strips the `.rsc` extension if it's in the pathname.
* Since this function is used on full urls it checks `?` for searchParams handling.
*/ export function normalizeRscURL(url) {
return url.replace(/\.rsc($|\?)/, // $1 ensures `?` is preserved
'$1');
}
//# sourceMappingURL=app-paths.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/app-paths.ts"],"sourcesContent":["import { ensureLeadingSlash } from '../../page-path/ensure-leading-slash'\nimport { isGroupSegment } from '../../segment'\n\n/**\n * Normalizes an app route so it represents the actual request path. Essentially\n * performing the following transformations:\n *\n * - `/(dashboard)/user/[id]/page` to `/user/[id]`\n * - `/(dashboard)/account/page` to `/account`\n * - `/user/[id]/page` to `/user/[id]`\n * - `/account/page` to `/account`\n * - `/page` to `/`\n * - `/(dashboard)/user/[id]/route` to `/user/[id]`\n * - `/(dashboard)/account/route` to `/account`\n * - `/user/[id]/route` to `/user/[id]`\n * - `/account/route` to `/account`\n * - `/route` to `/`\n * - `/` to `/`\n *\n * @param route the app route to normalize\n * @returns the normalized pathname\n */\nexport function normalizeAppPath(route: string) {\n return ensureLeadingSlash(\n route.split('/').reduce((pathname, segment, index, segments) => {\n // Empty segments are ignored.\n if (!segment) {\n return pathname\n }\n\n // Groups are ignored.\n if (isGroupSegment(segment)) {\n return pathname\n }\n\n // Parallel segments are ignored.\n if (segment[0] === '@') {\n return pathname\n }\n\n // The last segment (if it's a leaf) should be ignored.\n if (\n (segment === 'page' || segment === 'route') &&\n index === segments.length - 1\n ) {\n return pathname\n }\n\n return `${pathname}/${segment}`\n }, '')\n )\n}\n\n/**\n * Comparator for sorting app paths so that parallel slot paths (containing\n * `/@`) come before the children/root page path. This ensures the last item\n * is always the children page, which is what `renderPageComponent` reads via\n * `appPaths[appPaths.length - 1]`.\n *\n * Without this, route group prefixes like `(group)` (char code 0x28) sort\n * before `@` (0x40), causing the children page to sort first instead of last\n * and leading to a manifest mismatch / 404 in webpack dev mode.\n */\nexport function compareAppPaths(a: string, b: string): number {\n const aHasSlot = a.includes('/@')\n const bHasSlot = b.includes('/@')\n if (aHasSlot && !bHasSlot) return -1\n if (!aHasSlot && bHasSlot) return 1\n return a.localeCompare(b)\n}\n\n/**\n * Strips the `.rsc` extension if it's in the pathname.\n * Since this function is used on full urls it checks `?` for searchParams handling.\n */\nexport function normalizeRscURL(url: string) {\n return url.replace(\n /\\.rsc($|\\?)/,\n // $1 ensures `?` is preserved\n '$1'\n )\n}\n"],"names":["ensureLeadingSlash","isGroupSegment","normalizeAppPath","route","split","reduce","pathname","segment","index","segments","length","compareAppPaths","a","b","aHasSlot","includes","bHasSlot","localeCompare","normalizeRscURL","url","replace"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,uCAAsC;AACzE,SAASC,cAAc,QAAQ,gBAAe;AAE9C;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,OAAOH,mBACLG,MAAMC,KAAK,CAAC,KAAKC,MAAM,CAAC,CAACC,UAAUC,SAASC,OAAOC;QACjD,8BAA8B;QAC9B,IAAI,CAACF,SAAS;YACZ,OAAOD;QACT;QAEA,sBAAsB;QACtB,IAAIL,eAAeM,UAAU;YAC3B,OAAOD;QACT;QAEA,iCAAiC;QACjC,IAAIC,OAAO,CAAC,EAAE,KAAK,KAAK;YACtB,OAAOD;QACT;QAEA,uDAAuD;QACvD,IACE,AAACC,CAAAA,YAAY,UAAUA,YAAY,OAAM,KACzCC,UAAUC,SAASC,MAAM,GAAG,GAC5B;YACA,OAAOJ;QACT;QAEA,OAAO,GAAGA,SAAS,CAAC,EAAEC,SAAS;IACjC,GAAG;AAEP;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASI,gBAAgBC,CAAS,EAAEC,CAAS;IAClD,MAAMC,WAAWF,EAAEG,QAAQ,CAAC;IAC5B,MAAMC,WAAWH,EAAEE,QAAQ,CAAC;IAC5B,IAAID,YAAY,CAACE,UAAU,OAAO,CAAC;IACnC,IAAI,CAACF,YAAYE,UAAU,OAAO;IAClC,OAAOJ,EAAEK,aAAa,CAACJ;AACzB;AAEA;;;CAGC,GACD,OAAO,SAASK,gBAAgBC,GAAW;IACzC,OAAOA,IAAIC,OAAO,CAChB,eACA,8BAA8B;IAC9B;AAEJ","ignoreList":[0]}
@@ -0,0 +1,7 @@
// Convert router.asPath to a URLSearchParams object
// example: /dynamic/[slug]?foo=bar -> { foo: 'bar' }
export function asPathToSearchParams(asPath) {
return new URL(asPath, 'http://n').searchParams;
}
//# sourceMappingURL=as-path-to-search-params.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/as-path-to-search-params.ts"],"sourcesContent":["// Convert router.asPath to a URLSearchParams object\n// example: /dynamic/[slug]?foo=bar -> { foo: 'bar' }\nexport function asPathToSearchParams(asPath: string): URLSearchParams {\n return new URL(asPath, 'http://n').searchParams\n}\n"],"names":["asPathToSearchParams","asPath","URL","searchParams"],"mappings":"AAAA,oDAAoD;AACpD,qDAAqD;AACrD,OAAO,SAASA,qBAAqBC,MAAc;IACjD,OAAO,IAAIC,IAAID,QAAQ,YAAYE,YAAY;AACjD","ignoreList":[0]}
@@ -0,0 +1,14 @@
import { hexHash } from '../../hash';
export function computeCacheBustingSearchParam(prefetchHeader, segmentPrefetchHeader, stateTreeHeader, nextUrlHeader) {
if ((prefetchHeader === undefined || prefetchHeader === '0') && segmentPrefetchHeader === undefined && stateTreeHeader === undefined && nextUrlHeader === undefined) {
return '';
}
return hexHash([
prefetchHeader || '0',
segmentPrefetchHeader || '0',
stateTreeHeader || '0',
nextUrlHeader || '0'
].join(','));
}
//# sourceMappingURL=cache-busting-search-param.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/cache-busting-search-param.ts"],"sourcesContent":["import { hexHash } from '../../hash'\n\nexport function computeCacheBustingSearchParam(\n prefetchHeader: '1' | '2' | '0' | undefined,\n segmentPrefetchHeader: string | string[] | undefined,\n stateTreeHeader: string | string[] | undefined,\n nextUrlHeader: string | string[] | undefined\n): string {\n if (\n (prefetchHeader === undefined || prefetchHeader === '0') &&\n segmentPrefetchHeader === undefined &&\n stateTreeHeader === undefined &&\n nextUrlHeader === undefined\n ) {\n return ''\n }\n return hexHash(\n [\n prefetchHeader || '0',\n segmentPrefetchHeader || '0',\n stateTreeHeader || '0',\n nextUrlHeader || '0',\n ].join(',')\n )\n}\n"],"names":["hexHash","computeCacheBustingSearchParam","prefetchHeader","segmentPrefetchHeader","stateTreeHeader","nextUrlHeader","undefined","join"],"mappings":"AAAA,SAASA,OAAO,QAAQ,aAAY;AAEpC,OAAO,SAASC,+BACdC,cAA2C,EAC3CC,qBAAoD,EACpDC,eAA8C,EAC9CC,aAA4C;IAE5C,IACE,AAACH,CAAAA,mBAAmBI,aAAaJ,mBAAmB,GAAE,KACtDC,0BAA0BG,aAC1BF,oBAAoBE,aACpBD,kBAAkBC,WAClB;QACA,OAAO;IACT;IACA,OAAON,QACL;QACEE,kBAAkB;QAClBC,yBAAyB;QACzBC,mBAAmB;QACnBC,iBAAiB;KAClB,CAACE,IAAI,CAAC;AAEX","ignoreList":[0]}
+24
View File
@@ -0,0 +1,24 @@
export function compareRouterStates(a, b) {
const stateKeys = Object.keys(a);
if (stateKeys.length !== Object.keys(b).length) return false;
for(let i = stateKeys.length; i--;){
const key = stateKeys[i];
if (key === 'query') {
const queryKeys = Object.keys(a.query);
if (queryKeys.length !== Object.keys(b.query).length) {
return false;
}
for(let j = queryKeys.length; j--;){
const queryKey = queryKeys[j];
if (!b.query.hasOwnProperty(queryKey) || a.query[queryKey] !== b.query[queryKey]) {
return false;
}
}
} else if (!b.hasOwnProperty(key) || a[key] !== b[key]) {
return false;
}
}
return true;
}
//# sourceMappingURL=compare-states.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/compare-states.ts"],"sourcesContent":["import type { default as Router } from '../router'\n\nexport function compareRouterStates(a: Router['state'], b: Router['state']) {\n const stateKeys = Object.keys(a)\n if (stateKeys.length !== Object.keys(b).length) return false\n\n for (let i = stateKeys.length; i--; ) {\n const key = stateKeys[i]\n if (key === 'query') {\n const queryKeys = Object.keys(a.query)\n if (queryKeys.length !== Object.keys(b.query).length) {\n return false\n }\n for (let j = queryKeys.length; j--; ) {\n const queryKey = queryKeys[j]\n if (\n !b.query.hasOwnProperty(queryKey) ||\n a.query[queryKey] !== b.query[queryKey]\n ) {\n return false\n }\n }\n } else if (\n !b.hasOwnProperty(key) ||\n a[key as keyof Router['state']] !== b[key as keyof Router['state']]\n ) {\n return false\n }\n }\n\n return true\n}\n"],"names":["compareRouterStates","a","b","stateKeys","Object","keys","length","i","key","queryKeys","query","j","queryKey","hasOwnProperty"],"mappings":"AAEA,OAAO,SAASA,oBAAoBC,CAAkB,EAAEC,CAAkB;IACxE,MAAMC,YAAYC,OAAOC,IAAI,CAACJ;IAC9B,IAAIE,UAAUG,MAAM,KAAKF,OAAOC,IAAI,CAACH,GAAGI,MAAM,EAAE,OAAO;IAEvD,IAAK,IAAIC,IAAIJ,UAAUG,MAAM,EAAEC,KAAO;QACpC,MAAMC,MAAML,SAAS,CAACI,EAAE;QACxB,IAAIC,QAAQ,SAAS;YACnB,MAAMC,YAAYL,OAAOC,IAAI,CAACJ,EAAES,KAAK;YACrC,IAAID,UAAUH,MAAM,KAAKF,OAAOC,IAAI,CAACH,EAAEQ,KAAK,EAAEJ,MAAM,EAAE;gBACpD,OAAO;YACT;YACA,IAAK,IAAIK,IAAIF,UAAUH,MAAM,EAAEK,KAAO;gBACpC,MAAMC,WAAWH,SAAS,CAACE,EAAE;gBAC7B,IACE,CAACT,EAAEQ,KAAK,CAACG,cAAc,CAACD,aACxBX,EAAES,KAAK,CAACE,SAAS,KAAKV,EAAEQ,KAAK,CAACE,SAAS,EACvC;oBACA,OAAO;gBACT;YACF;QACF,OAAO,IACL,CAACV,EAAEW,cAAc,CAACL,QAClBP,CAAC,CAACO,IAA6B,KAAKN,CAAC,CAACM,IAA6B,EACnE;YACA,OAAO;QACT;IACF;IAEA,OAAO;AACT","ignoreList":[0]}
@@ -0,0 +1,36 @@
import { warnOnce } from '../../utils/warn-once';
/**
* Run function with `scroll-behavior: auto` applied to `<html/>`.
* This css change will be reverted after the function finishes.
*/ export function disableSmoothScrollDuringRouteTransition(fn, options = {}) {
// if only the hash is changed, we don't need to disable smooth scrolling
// we only care to prevent smooth scrolling when navigating to a new page to avoid jarring UX
if (options.onlyHashChange) {
fn();
return;
}
const htmlElement = document.documentElement;
const hasDataAttribute = htmlElement.dataset.scrollBehavior === 'smooth';
if (!hasDataAttribute) {
// Warn if smooth scrolling is detected but no data attribute is present
if (process.env.NODE_ENV === 'development' && getComputedStyle(htmlElement).scrollBehavior === 'smooth') {
warnOnce('Detected `scroll-behavior: smooth` on the `<html>` element. To disable smooth scrolling during route transitions, ' + 'add `data-scroll-behavior="smooth"` to your <html> element. ' + 'Learn more: https://nextjs.org/docs/messages/missing-data-scroll-behavior');
}
// No smooth scrolling configured, run directly without style manipulation
fn();
return;
}
// Proceed with temporarily disabling smooth scrolling
const existing = htmlElement.style.scrollBehavior;
htmlElement.style.scrollBehavior = 'auto';
if (!options.dontForceLayout) {
// In Chrome-based browsers we need to force reflow before calling `scrollTo`.
// Otherwise it will not pickup the change in scrollBehavior
// More info here: https://github.com/vercel/next.js/issues/40719#issuecomment-1336248042
htmlElement.getClientRects();
}
fn();
htmlElement.style.scrollBehavior = existing;
}
//# sourceMappingURL=disable-smooth-scroll.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/disable-smooth-scroll.ts"],"sourcesContent":["import { warnOnce } from '../../utils/warn-once'\n\n/**\n * Run function with `scroll-behavior: auto` applied to `<html/>`.\n * This css change will be reverted after the function finishes.\n */\nexport function disableSmoothScrollDuringRouteTransition(\n fn: () => void,\n options: { dontForceLayout?: boolean; onlyHashChange?: boolean } = {}\n) {\n // if only the hash is changed, we don't need to disable smooth scrolling\n // we only care to prevent smooth scrolling when navigating to a new page to avoid jarring UX\n if (options.onlyHashChange) {\n fn()\n return\n }\n\n const htmlElement = document.documentElement\n const hasDataAttribute = htmlElement.dataset.scrollBehavior === 'smooth'\n\n if (!hasDataAttribute) {\n // Warn if smooth scrolling is detected but no data attribute is present\n if (\n process.env.NODE_ENV === 'development' &&\n getComputedStyle(htmlElement).scrollBehavior === 'smooth'\n ) {\n warnOnce(\n 'Detected `scroll-behavior: smooth` on the `<html>` element. To disable smooth scrolling during route transitions, ' +\n 'add `data-scroll-behavior=\"smooth\"` to your <html> element. ' +\n 'Learn more: https://nextjs.org/docs/messages/missing-data-scroll-behavior'\n )\n }\n // No smooth scrolling configured, run directly without style manipulation\n fn()\n return\n }\n\n // Proceed with temporarily disabling smooth scrolling\n const existing = htmlElement.style.scrollBehavior\n htmlElement.style.scrollBehavior = 'auto'\n if (!options.dontForceLayout) {\n // In Chrome-based browsers we need to force reflow before calling `scrollTo`.\n // Otherwise it will not pickup the change in scrollBehavior\n // More info here: https://github.com/vercel/next.js/issues/40719#issuecomment-1336248042\n htmlElement.getClientRects()\n }\n fn()\n htmlElement.style.scrollBehavior = existing\n}\n"],"names":["warnOnce","disableSmoothScrollDuringRouteTransition","fn","options","onlyHashChange","htmlElement","document","documentElement","hasDataAttribute","dataset","scrollBehavior","process","env","NODE_ENV","getComputedStyle","existing","style","dontForceLayout","getClientRects"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,wBAAuB;AAEhD;;;CAGC,GACD,OAAO,SAASC,yCACdC,EAAc,EACdC,UAAmE,CAAC,CAAC;IAErE,yEAAyE;IACzE,6FAA6F;IAC7F,IAAIA,QAAQC,cAAc,EAAE;QAC1BF;QACA;IACF;IAEA,MAAMG,cAAcC,SAASC,eAAe;IAC5C,MAAMC,mBAAmBH,YAAYI,OAAO,CAACC,cAAc,KAAK;IAEhE,IAAI,CAACF,kBAAkB;QACrB,wEAAwE;QACxE,IACEG,QAAQC,GAAG,CAACC,QAAQ,KAAK,iBACzBC,iBAAiBT,aAAaK,cAAc,KAAK,UACjD;YACAV,SACE,uHACE,iEACA;QAEN;QACA,0EAA0E;QAC1EE;QACA;IACF;IAEA,sDAAsD;IACtD,MAAMa,WAAWV,YAAYW,KAAK,CAACN,cAAc;IACjDL,YAAYW,KAAK,CAACN,cAAc,GAAG;IACnC,IAAI,CAACP,QAAQc,eAAe,EAAE;QAC5B,8EAA8E;QAC9E,4DAA4D;QAC5D,yFAAyF;QACzFZ,YAAYa,cAAc;IAC5B;IACAhB;IACAG,YAAYW,KAAK,CAACN,cAAc,GAAGK;AACrC","ignoreList":[0]}
@@ -0,0 +1,6 @@
// escape delimiters used by path-to-regexp
export default function escapePathDelimiters(segment, escapeEncoded) {
return segment.replace(new RegExp(`([/#?]${escapeEncoded ? '|%(2f|23|3f|5c)' : ''})`, 'gi'), (char)=>encodeURIComponent(char));
}
//# sourceMappingURL=escape-path-delimiters.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/escape-path-delimiters.ts"],"sourcesContent":["// escape delimiters used by path-to-regexp\nexport default function escapePathDelimiters(\n segment: string,\n escapeEncoded?: boolean\n): string {\n return segment.replace(\n new RegExp(`([/#?]${escapeEncoded ? '|%(2f|23|3f|5c)' : ''})`, 'gi'),\n (char: string) => encodeURIComponent(char)\n )\n}\n"],"names":["escapePathDelimiters","segment","escapeEncoded","replace","RegExp","char","encodeURIComponent"],"mappings":"AAAA,2CAA2C;AAC3C,eAAe,SAASA,qBACtBC,OAAe,EACfC,aAAuB;IAEvB,OAAOD,QAAQE,OAAO,CACpB,IAAIC,OAAO,CAAC,MAAM,EAAEF,gBAAgB,oBAAoB,GAAG,CAAC,CAAC,EAAE,OAC/D,CAACG,OAAiBC,mBAAmBD;AAEzC","ignoreList":[0]}
@@ -0,0 +1,17 @@
import { removeTrailingSlash } from './remove-trailing-slash';
import { addPathPrefix } from './add-path-prefix';
import { addPathSuffix } from './add-path-suffix';
import { addLocale } from './add-locale';
export function formatNextPathnameInfo(info) {
let pathname = addLocale(info.pathname, info.locale, info.buildId ? undefined : info.defaultLocale, info.ignorePrefix);
if (info.buildId || !info.trailingSlash) {
pathname = removeTrailingSlash(pathname);
}
if (info.buildId) {
pathname = addPathSuffix(addPathPrefix(pathname, `/_next/data/${info.buildId}`), info.pathname === '/' ? 'index.json' : '.json');
}
pathname = addPathPrefix(pathname, info.basePath);
return !info.buildId && info.trailingSlash ? !pathname.endsWith('/') ? addPathSuffix(pathname, '/') : pathname : removeTrailingSlash(pathname);
}
//# sourceMappingURL=format-next-pathname-info.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/format-next-pathname-info.ts"],"sourcesContent":["import type { NextPathnameInfo } from './get-next-pathname-info'\nimport { removeTrailingSlash } from './remove-trailing-slash'\nimport { addPathPrefix } from './add-path-prefix'\nimport { addPathSuffix } from './add-path-suffix'\nimport { addLocale } from './add-locale'\n\ninterface ExtendedInfo extends NextPathnameInfo {\n defaultLocale?: string\n ignorePrefix?: boolean\n}\n\nexport function formatNextPathnameInfo(info: ExtendedInfo) {\n let pathname = addLocale(\n info.pathname,\n info.locale,\n info.buildId ? undefined : info.defaultLocale,\n info.ignorePrefix\n )\n\n if (info.buildId || !info.trailingSlash) {\n pathname = removeTrailingSlash(pathname)\n }\n\n if (info.buildId) {\n pathname = addPathSuffix(\n addPathPrefix(pathname, `/_next/data/${info.buildId}`),\n info.pathname === '/' ? 'index.json' : '.json'\n )\n }\n\n pathname = addPathPrefix(pathname, info.basePath)\n return !info.buildId && info.trailingSlash\n ? !pathname.endsWith('/')\n ? addPathSuffix(pathname, '/')\n : pathname\n : removeTrailingSlash(pathname)\n}\n"],"names":["removeTrailingSlash","addPathPrefix","addPathSuffix","addLocale","formatNextPathnameInfo","info","pathname","locale","buildId","undefined","defaultLocale","ignorePrefix","trailingSlash","basePath","endsWith"],"mappings":"AACA,SAASA,mBAAmB,QAAQ,0BAAyB;AAC7D,SAASC,aAAa,QAAQ,oBAAmB;AACjD,SAASC,aAAa,QAAQ,oBAAmB;AACjD,SAASC,SAAS,QAAQ,eAAc;AAOxC,OAAO,SAASC,uBAAuBC,IAAkB;IACvD,IAAIC,WAAWH,UACbE,KAAKC,QAAQ,EACbD,KAAKE,MAAM,EACXF,KAAKG,OAAO,GAAGC,YAAYJ,KAAKK,aAAa,EAC7CL,KAAKM,YAAY;IAGnB,IAAIN,KAAKG,OAAO,IAAI,CAACH,KAAKO,aAAa,EAAE;QACvCN,WAAWN,oBAAoBM;IACjC;IAEA,IAAID,KAAKG,OAAO,EAAE;QAChBF,WAAWJ,cACTD,cAAcK,UAAU,CAAC,YAAY,EAAED,KAAKG,OAAO,EAAE,GACrDH,KAAKC,QAAQ,KAAK,MAAM,eAAe;IAE3C;IAEAA,WAAWL,cAAcK,UAAUD,KAAKQ,QAAQ;IAChD,OAAO,CAACR,KAAKG,OAAO,IAAIH,KAAKO,aAAa,GACtC,CAACN,SAASQ,QAAQ,CAAC,OACjBZ,cAAcI,UAAU,OACxBA,WACFN,oBAAoBM;AAC1B","ignoreList":[0]}
+84
View File
@@ -0,0 +1,84 @@
// Format function modified from nodejs
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import * as querystring from './querystring';
const slashedProtocols = /https?|ftp|gopher|file/;
export function formatUrl(urlObj) {
let { auth, hostname } = urlObj;
let protocol = urlObj.protocol || '';
let pathname = urlObj.pathname || '';
let hash = urlObj.hash || '';
let query = urlObj.query || '';
let host = false;
auth = auth ? encodeURIComponent(auth).replace(/%3A/i, ':') + '@' : '';
if (urlObj.host) {
host = auth + urlObj.host;
} else if (hostname) {
host = auth + (~hostname.indexOf(':') ? `[${hostname}]` : hostname);
if (urlObj.port) {
host += ':' + urlObj.port;
}
}
if (query && typeof query === 'object') {
query = String(querystring.urlQueryToSearchParams(query));
}
let search = urlObj.search || query && `?${query}` || '';
if (protocol && !protocol.endsWith(':')) protocol += ':';
if (urlObj.slashes || (!protocol || slashedProtocols.test(protocol)) && host !== false) {
host = '//' + (host || '');
if (pathname && pathname[0] !== '/') pathname = '/' + pathname;
} else if (!host) {
host = '';
}
if (hash && hash[0] !== '#') hash = '#' + hash;
if (search && search[0] !== '?') search = '?' + search;
pathname = pathname.replace(/[?#]/g, encodeURIComponent);
search = search.replace('#', '%23');
return `${protocol}${host}${pathname}${search}${hash}`;
}
export const urlObjectKeys = [
'auth',
'hash',
'host',
'hostname',
'href',
'path',
'pathname',
'port',
'protocol',
'query',
'search',
'slashes'
];
export function formatWithValidation(url) {
if (process.env.NODE_ENV === 'development') {
if (url !== null && typeof url === 'object') {
Object.keys(url).forEach((key)=>{
if (!urlObjectKeys.includes(key)) {
console.warn(`Unknown key passed via urlObject into url.format: ${key}`);
}
});
}
}
return formatUrl(url);
}
//# sourceMappingURL=format-url.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
// Translates a logical route into its pages asset path (relative from a common prefix)
// "asset path" being its javascript file, data file, prerendered html,...
export default function getAssetPathFromRoute(route, ext = '') {
const path = route === '/' ? '/index' : /^\/index(\/|$)/.test(route) ? `/index${route}` : route;
return path + ext;
}
//# sourceMappingURL=get-asset-path-from-route.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/get-asset-path-from-route.ts"],"sourcesContent":["// Translates a logical route into its pages asset path (relative from a common prefix)\n// \"asset path\" being its javascript file, data file, prerendered html,...\nexport default function getAssetPathFromRoute(\n route: string,\n ext: string = ''\n): string {\n const path =\n route === '/'\n ? '/index'\n : /^\\/index(\\/|$)/.test(route)\n ? `/index${route}`\n : route\n return path + ext\n}\n"],"names":["getAssetPathFromRoute","route","ext","path","test"],"mappings":"AAAA,uFAAuF;AACvF,0EAA0E;AAC1E,eAAe,SAASA,sBACtBC,KAAa,EACbC,MAAc,EAAE;IAEhB,MAAMC,OACJF,UAAU,MACN,WACA,iBAAiBG,IAAI,CAACH,SACpB,CAAC,MAAM,EAAEA,OAAO,GAChBA;IACR,OAAOE,OAAOD;AAChB","ignoreList":[0]}
+179
View File
@@ -0,0 +1,179 @@
import { InvariantError } from '../../invariant-error';
import { parseLoaderTree } from './parse-loader-tree';
import { parseAppRoute, parseAppRouteSegment } from '../routes/app';
import { resolveParamValue } from './resolve-param-value';
/**
* Gets the value of a param from the params object. This correctly handles the
* case where the param is a fallback route param and encodes the resulting
* value.
*
* @param interpolatedParams - The params object.
* @param segmentKey - The key of the segment.
* @param fallbackRouteParams - The fallback route params.
* @returns The value of the param.
*/ function getParamValue(interpolatedParams, segmentKey, fallbackRouteParams) {
let value = interpolatedParams[segmentKey];
if (fallbackRouteParams?.has(segmentKey)) {
// We know that the fallback route params has the segment key because we
// checked that above.
const [searchValue] = fallbackRouteParams.get(segmentKey);
value = searchValue;
} else if (Array.isArray(value)) {
value = value.map((i)=>encodeURIComponent(i));
} else if (typeof value === 'string') {
value = encodeURIComponent(value);
}
return value;
}
export function interpolateParallelRouteParams(loaderTree, params, pagePath, fallbackRouteParams) {
const interpolated = structuredClone(params);
// Stack-based traversal with depth tracking
const stack = [
{
tree: loaderTree,
depth: 0
}
];
// Parse the route from the provided page path.
const route = parseAppRoute(pagePath, true);
while(stack.length > 0){
const { tree, depth } = stack.pop();
const { segment, parallelRoutes } = parseLoaderTree(tree);
const appSegment = parseAppRouteSegment(segment);
if (appSegment?.type === 'dynamic' && !interpolated.hasOwnProperty(appSegment.param.paramName) && // If the param is in the fallback route params, we don't need to
// interpolate it because it's already marked as being unknown.
!fallbackRouteParams?.has(appSegment.param.paramName)) {
const { paramName, paramType } = appSegment.param;
const paramValue = resolveParamValue(paramName, paramType, depth, route, interpolated);
if (paramValue !== undefined) {
interpolated[paramName] = paramValue;
} else if (paramType !== 'optional-catchall') {
throw Object.defineProperty(new InvariantError(`Could not resolve param value for segment: ${paramName}`), "__NEXT_ERROR_CODE", {
value: "E932",
enumerable: false,
configurable: true
});
}
}
// Calculate next depth - increment if this is not a route group and not empty
let nextDepth = depth;
if (appSegment && appSegment.type !== 'route-group' && appSegment.type !== 'parallel-route') {
nextDepth++;
}
// Add all parallel routes to the stack for processing
for (const parallelRoute of Object.values(parallelRoutes)){
stack.push({
tree: parallelRoute,
depth: nextDepth
});
}
}
return interpolated;
}
/**
*
* Shared logic on client and server for creating a dynamic param value.
*
* This code needs to be shared with the client so it can extract dynamic route
* params from the URL without a server request.
*
* Because everything in this module is sent to the client, we should aim to
* keep this code as simple as possible. The special case handling for catchall
* and optional is, alas, unfortunate.
*/ export function getDynamicParam(interpolatedParams, segmentKey, dynamicParamType, fallbackRouteParams, staticSiblings) {
let value = getParamValue(interpolatedParams, segmentKey, fallbackRouteParams);
// handle the case where an optional catchall does not have a value,
// e.g. `/dashboard/[[...slug]]` when requesting `/dashboard`
if (!value || value.length === 0) {
if (dynamicParamType === 'oc') {
return {
param: segmentKey,
value: null,
type: dynamicParamType,
treeSegment: [
segmentKey,
'',
dynamicParamType,
staticSiblings
]
};
}
throw Object.defineProperty(new InvariantError(`Missing value for segment key: "${segmentKey}" with dynamic param type: ${dynamicParamType}`), "__NEXT_ERROR_CODE", {
value: "E864",
enumerable: false,
configurable: true
});
}
const paramCacheKey = Array.isArray(value) ? value.join('/') : value;
return {
param: segmentKey,
// The value that is passed to user code.
value,
// The value that is rendered in the router tree.
// TODO: If the number of static siblings exceeds some threshold (e.g.,
// dozens or hundreds), consider sending a Bloom filter instead of the full
// array to reduce payload size. The client would then use the Bloom filter
// to check membership with a small false positive rate.
treeSegment: [
segmentKey,
paramCacheKey,
dynamicParamType,
staticSiblings
],
type: dynamicParamType
};
}
/**
* Regular expression pattern used to match route parameters.
* Matches both single parameters and parameter groups.
* Examples:
* - `[[...slug]]` matches parameter group with key 'slug', repeat: true, optional: true
* - `[...slug]` matches parameter group with key 'slug', repeat: true, optional: false
* - `[[foo]]` matches parameter with key 'foo', repeat: false, optional: true
* - `[bar]` matches parameter with key 'bar', repeat: false, optional: false
*/ export const PARAMETER_PATTERN = /^([^[]*)\[((?:\[[^\]]*\])|[^\]]+)\](.*)$/;
/**
* Parses a given parameter from a route to a data structure that can be used
* to generate the parametrized route.
* Examples:
* - `[[...slug]]` -> `{ key: 'slug', repeat: true, optional: true }`
* - `[...slug]` -> `{ key: 'slug', repeat: true, optional: false }`
* - `[[foo]]` -> `{ key: 'foo', repeat: false, optional: true }`
* - `[bar]` -> `{ key: 'bar', repeat: false, optional: false }`
* - `fizz` -> `{ key: 'fizz', repeat: false, optional: false }`
* @param param - The parameter to parse.
* @returns The parsed parameter as a data structure.
*/ export function parseParameter(param) {
const match = param.match(PARAMETER_PATTERN);
if (!match) {
return parseMatchedParameter(param);
}
return parseMatchedParameter(match[2]);
}
/**
* Parses a matched parameter from the PARAMETER_PATTERN regex to a data structure that can be used
* to generate the parametrized route.
* Examples:
* - `[...slug]` -> `{ key: 'slug', repeat: true, optional: true }`
* - `...slug` -> `{ key: 'slug', repeat: true, optional: false }`
* - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }`
* - `bar` -> `{ key: 'bar', repeat: false, optional: false }`
* @param param - The matched parameter to parse.
* @returns The parsed parameter as a data structure.
*/ export function parseMatchedParameter(param) {
const optional = param.startsWith('[') && param.endsWith(']');
if (optional) {
param = param.slice(1, -1);
}
const repeat = param.startsWith('...');
if (repeat) {
param = param.slice(3);
}
return {
key: param,
repeat,
optional
};
}
//# sourceMappingURL=get-dynamic-param.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
import { normalizeLocalePath } from '../../i18n/normalize-locale-path';
import { removePathPrefix } from './remove-path-prefix';
import { pathHasPrefix } from './path-has-prefix';
export function getNextPathnameInfo(pathname, options) {
const { basePath, i18n, trailingSlash } = options.nextConfig ?? {};
const info = {
pathname,
trailingSlash: pathname !== '/' ? pathname.endsWith('/') : trailingSlash
};
if (basePath && pathHasPrefix(info.pathname, basePath)) {
info.pathname = removePathPrefix(info.pathname, basePath);
info.basePath = basePath;
}
let pathnameNoDataPrefix = info.pathname;
if (info.pathname.startsWith('/_next/data/') && info.pathname.endsWith('.json')) {
const paths = info.pathname.replace(/^\/_next\/data\//, '').replace(/\.json$/, '').split('/');
const buildId = paths[0];
info.buildId = buildId;
pathnameNoDataPrefix = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/';
// update pathname with normalized if enabled although
// we use normalized to populate locale info still
if (options.parseData === true) {
info.pathname = pathnameNoDataPrefix;
}
}
// If provided, use the locale route normalizer to detect the locale instead
// of the function below.
if (i18n) {
let result = options.i18nProvider ? options.i18nProvider.analyze(info.pathname) : normalizeLocalePath(info.pathname, i18n.locales);
info.locale = result.detectedLocale;
info.pathname = result.pathname ?? info.pathname;
if (!result.detectedLocale && info.buildId) {
result = options.i18nProvider ? options.i18nProvider.analyze(pathnameNoDataPrefix) : normalizeLocalePath(pathnameNoDataPrefix, i18n.locales);
if (result.detectedLocale) {
info.locale = result.detectedLocale;
}
}
}
return info;
}
//# sourceMappingURL=get-next-pathname-info.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
// Translate a pages asset path (relative from a common prefix) back into its logical route
import { isDynamicRoute } from './is-dynamic';
// "asset path" being its javascript file, data file, prerendered html,...
export default function getRouteFromAssetPath(assetPath, ext = '') {
assetPath = assetPath.replace(/\\/g, '/');
assetPath = ext && assetPath.endsWith(ext) ? assetPath.slice(0, -ext.length) : assetPath;
if (assetPath.startsWith('/index/') && !isDynamicRoute(assetPath)) {
assetPath = assetPath.slice(6);
} else if (assetPath === '/index') {
assetPath = '/';
}
return assetPath;
}
//# sourceMappingURL=get-route-from-asset-path.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/get-route-from-asset-path.ts"],"sourcesContent":["// Translate a pages asset path (relative from a common prefix) back into its logical route\n\nimport { isDynamicRoute } from './is-dynamic'\n\n// \"asset path\" being its javascript file, data file, prerendered html,...\nexport default function getRouteFromAssetPath(\n assetPath: string,\n ext: string = ''\n): string {\n assetPath = assetPath.replace(/\\\\/g, '/')\n assetPath =\n ext && assetPath.endsWith(ext) ? assetPath.slice(0, -ext.length) : assetPath\n if (assetPath.startsWith('/index/') && !isDynamicRoute(assetPath)) {\n assetPath = assetPath.slice(6)\n } else if (assetPath === '/index') {\n assetPath = '/'\n }\n return assetPath\n}\n"],"names":["isDynamicRoute","getRouteFromAssetPath","assetPath","ext","replace","endsWith","slice","length","startsWith"],"mappings":"AAAA,2FAA2F;AAE3F,SAASA,cAAc,QAAQ,eAAc;AAE7C,0EAA0E;AAC1E,eAAe,SAASC,sBACtBC,SAAiB,EACjBC,MAAc,EAAE;IAEhBD,YAAYA,UAAUE,OAAO,CAAC,OAAO;IACrCF,YACEC,OAAOD,UAAUG,QAAQ,CAACF,OAAOD,UAAUI,KAAK,CAAC,GAAG,CAACH,IAAII,MAAM,IAAIL;IACrE,IAAIA,UAAUM,UAAU,CAAC,cAAc,CAACR,eAAeE,YAAY;QACjEA,YAAYA,UAAUI,KAAK,CAAC;IAC9B,OAAO,IAAIJ,cAAc,UAAU;QACjCA,YAAY;IACd;IACA,OAAOA;AACT","ignoreList":[0]}
@@ -0,0 +1,66 @@
import { INTERCEPTION_ROUTE_MARKERS } from './interception-routes';
/**
* Parse dynamic route segment to type of parameter
*/ export function getSegmentParam(segment) {
const interceptionMarker = INTERCEPTION_ROUTE_MARKERS.find((marker)=>segment.startsWith(marker));
// if an interception marker is part of the path segment, we need to jump ahead
// to the relevant portion for param parsing
if (interceptionMarker) {
segment = segment.slice(interceptionMarker.length);
}
if (segment.startsWith('[[...') && segment.endsWith(']]')) {
return {
// TODO-APP: Optional catchall does not currently work with parallel routes,
// so for now aren't handling a potential interception marker.
paramType: 'optional-catchall',
paramName: segment.slice(5, -2)
};
}
if (segment.startsWith('[...') && segment.endsWith(']')) {
return {
paramType: interceptionMarker ? `catchall-intercepted-${interceptionMarker}` : 'catchall',
paramName: segment.slice(4, -1)
};
}
if (segment.startsWith('[') && segment.endsWith(']')) {
return {
paramType: interceptionMarker ? `dynamic-intercepted-${interceptionMarker}` : 'dynamic',
paramName: segment.slice(1, -1)
};
}
return null;
}
export function isCatchAll(type) {
return type === 'catchall' || type === 'catchall-intercepted-(..)(..)' || type === 'catchall-intercepted-(.)' || type === 'catchall-intercepted-(..)' || type === 'catchall-intercepted-(...)' || type === 'optional-catchall';
}
export function getParamProperties(paramType) {
let repeat = false;
let optional = false;
switch(paramType){
case 'catchall':
case 'catchall-intercepted-(..)(..)':
case 'catchall-intercepted-(.)':
case 'catchall-intercepted-(..)':
case 'catchall-intercepted-(...)':
repeat = true;
break;
case 'optional-catchall':
repeat = true;
optional = true;
break;
case 'dynamic':
case 'dynamic-intercepted-(..)(..)':
case 'dynamic-intercepted-(.)':
case 'dynamic-intercepted-(..)':
case 'dynamic-intercepted-(...)':
break;
default:
paramType;
}
return {
repeat,
optional
};
}
//# sourceMappingURL=get-segment-param.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/get-segment-param.tsx"],"sourcesContent":["import { INTERCEPTION_ROUTE_MARKERS } from './interception-routes'\nimport type { DynamicParamTypes } from '../../app-router-types'\n\nexport type SegmentParam = {\n paramName: string\n paramType: DynamicParamTypes\n}\n\n/**\n * Parse dynamic route segment to type of parameter\n */\nexport function getSegmentParam(segment: string): SegmentParam | null {\n const interceptionMarker = INTERCEPTION_ROUTE_MARKERS.find((marker) =>\n segment.startsWith(marker)\n )\n\n // if an interception marker is part of the path segment, we need to jump ahead\n // to the relevant portion for param parsing\n if (interceptionMarker) {\n segment = segment.slice(interceptionMarker.length)\n }\n\n if (segment.startsWith('[[...') && segment.endsWith(']]')) {\n return {\n // TODO-APP: Optional catchall does not currently work with parallel routes,\n // so for now aren't handling a potential interception marker.\n paramType: 'optional-catchall',\n paramName: segment.slice(5, -2),\n }\n }\n\n if (segment.startsWith('[...') && segment.endsWith(']')) {\n return {\n paramType: interceptionMarker\n ? `catchall-intercepted-${interceptionMarker}`\n : 'catchall',\n paramName: segment.slice(4, -1),\n }\n }\n\n if (segment.startsWith('[') && segment.endsWith(']')) {\n return {\n paramType: interceptionMarker\n ? `dynamic-intercepted-${interceptionMarker}`\n : 'dynamic',\n paramName: segment.slice(1, -1),\n }\n }\n\n return null\n}\n\nexport function isCatchAll(\n type: DynamicParamTypes\n): type is\n | 'catchall'\n | 'catchall-intercepted-(..)(..)'\n | 'catchall-intercepted-(.)'\n | 'catchall-intercepted-(..)'\n | 'catchall-intercepted-(...)'\n | 'optional-catchall' {\n return (\n type === 'catchall' ||\n type === 'catchall-intercepted-(..)(..)' ||\n type === 'catchall-intercepted-(.)' ||\n type === 'catchall-intercepted-(..)' ||\n type === 'catchall-intercepted-(...)' ||\n type === 'optional-catchall'\n )\n}\n\nexport function getParamProperties(paramType: DynamicParamTypes): {\n repeat: boolean\n optional: boolean\n} {\n let repeat = false\n let optional = false\n\n switch (paramType) {\n case 'catchall':\n case 'catchall-intercepted-(..)(..)':\n case 'catchall-intercepted-(.)':\n case 'catchall-intercepted-(..)':\n case 'catchall-intercepted-(...)':\n repeat = true\n break\n case 'optional-catchall':\n repeat = true\n optional = true\n break\n case 'dynamic':\n case 'dynamic-intercepted-(..)(..)':\n case 'dynamic-intercepted-(.)':\n case 'dynamic-intercepted-(..)':\n case 'dynamic-intercepted-(...)':\n break\n default:\n paramType satisfies never\n }\n\n return { repeat, optional }\n}\n"],"names":["INTERCEPTION_ROUTE_MARKERS","getSegmentParam","segment","interceptionMarker","find","marker","startsWith","slice","length","endsWith","paramType","paramName","isCatchAll","type","getParamProperties","repeat","optional"],"mappings":"AAAA,SAASA,0BAA0B,QAAQ,wBAAuB;AAQlE;;CAEC,GACD,OAAO,SAASC,gBAAgBC,OAAe;IAC7C,MAAMC,qBAAqBH,2BAA2BI,IAAI,CAAC,CAACC,SAC1DH,QAAQI,UAAU,CAACD;IAGrB,+EAA+E;IAC/E,4CAA4C;IAC5C,IAAIF,oBAAoB;QACtBD,UAAUA,QAAQK,KAAK,CAACJ,mBAAmBK,MAAM;IACnD;IAEA,IAAIN,QAAQI,UAAU,CAAC,YAAYJ,QAAQO,QAAQ,CAAC,OAAO;QACzD,OAAO;YACL,4EAA4E;YAC5E,8DAA8D;YAC9DC,WAAW;YACXC,WAAWT,QAAQK,KAAK,CAAC,GAAG,CAAC;QAC/B;IACF;IAEA,IAAIL,QAAQI,UAAU,CAAC,WAAWJ,QAAQO,QAAQ,CAAC,MAAM;QACvD,OAAO;YACLC,WAAWP,qBACP,CAAC,qBAAqB,EAAEA,oBAAoB,GAC5C;YACJQ,WAAWT,QAAQK,KAAK,CAAC,GAAG,CAAC;QAC/B;IACF;IAEA,IAAIL,QAAQI,UAAU,CAAC,QAAQJ,QAAQO,QAAQ,CAAC,MAAM;QACpD,OAAO;YACLC,WAAWP,qBACP,CAAC,oBAAoB,EAAEA,oBAAoB,GAC3C;YACJQ,WAAWT,QAAQK,KAAK,CAAC,GAAG,CAAC;QAC/B;IACF;IAEA,OAAO;AACT;AAEA,OAAO,SAASK,WACdC,IAAuB;IAQvB,OACEA,SAAS,cACTA,SAAS,mCACTA,SAAS,8BACTA,SAAS,+BACTA,SAAS,gCACTA,SAAS;AAEb;AAEA,OAAO,SAASC,mBAAmBJ,SAA4B;IAI7D,IAAIK,SAAS;IACb,IAAIC,WAAW;IAEf,OAAQN;QACN,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;YACHK,SAAS;YACT;QACF,KAAK;YACHA,SAAS;YACTC,WAAW;YACX;QACF,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;YACH;QACF;YACEN;IACJ;IAEA,OAAO;QAAEK;QAAQC;IAAS;AAC5B","ignoreList":[0]}
+7
View File
@@ -0,0 +1,7 @@
// This regex contains the bots that we need to do a blocking render for and can't safely stream the response
// due to how they parse the DOM. For example, they might explicitly check for metadata in the `head` tag, so we can't stream metadata tags after the `head` was sent.
// Note: The pattern [\w-]+-Google captures all Google crawlers with "-Google" suffix (e.g., Mediapartners-Google, AdsBot-Google, Storebot-Google)
// as well as crawlers starting with "Google-" (e.g., Google-PageRenderer, Google-InspectionTool)
export const HTML_LIMITED_BOT_UA_RE = /[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i;
//# sourceMappingURL=html-bots.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/html-bots.ts"],"sourcesContent":["// This regex contains the bots that we need to do a blocking render for and can't safely stream the response\n// due to how they parse the DOM. For example, they might explicitly check for metadata in the `head` tag, so we can't stream metadata tags after the `head` was sent.\n// Note: The pattern [\\w-]+-Google captures all Google crawlers with \"-Google\" suffix (e.g., Mediapartners-Google, AdsBot-Google, Storebot-Google)\n// as well as crawlers starting with \"Google-\" (e.g., Google-PageRenderer, Google-InspectionTool)\nexport const HTML_LIMITED_BOT_UA_RE =\n /[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i\n"],"names":["HTML_LIMITED_BOT_UA_RE"],"mappings":"AAAA,6GAA6G;AAC7G,sKAAsK;AACtK,kJAAkJ;AAClJ,iGAAiG;AACjG,OAAO,MAAMA,yBACX,sTAAqT","ignoreList":[0]}
+4
View File
@@ -0,0 +1,4 @@
export { getSortedRoutes, getSortedRouteObjects } from './sorted-routes';
export { isDynamicRoute } from './is-dynamic';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/index.ts"],"sourcesContent":["export { getSortedRoutes, getSortedRouteObjects } from './sorted-routes'\nexport { isDynamicRoute } from './is-dynamic'\n"],"names":["getSortedRoutes","getSortedRouteObjects","isDynamicRoute"],"mappings":"AAAA,SAASA,eAAe,EAAEC,qBAAqB,QAAQ,kBAAiB;AACxE,SAASC,cAAc,QAAQ,eAAc","ignoreList":[0]}
@@ -0,0 +1,23 @@
export function interceptionPrefixFromParamType(paramType) {
switch(paramType){
case 'catchall-intercepted-(..)(..)':
case 'dynamic-intercepted-(..)(..)':
return '(..)(..)';
case 'catchall-intercepted-(.)':
case 'dynamic-intercepted-(.)':
return '(.)';
case 'catchall-intercepted-(..)':
case 'dynamic-intercepted-(..)':
return '(..)';
case 'catchall-intercepted-(...)':
case 'dynamic-intercepted-(...)':
return '(...)';
case 'catchall':
case 'dynamic':
case 'optional-catchall':
default:
return null;
}
}
//# sourceMappingURL=interception-prefix-from-param-type.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/interception-prefix-from-param-type.ts"],"sourcesContent":["import type { DynamicParamTypes } from '../../app-router-types'\n\nexport function interceptionPrefixFromParamType(\n paramType: DynamicParamTypes\n): string | null {\n switch (paramType) {\n case 'catchall-intercepted-(..)(..)':\n case 'dynamic-intercepted-(..)(..)':\n return '(..)(..)'\n case 'catchall-intercepted-(.)':\n case 'dynamic-intercepted-(.)':\n return '(.)'\n case 'catchall-intercepted-(..)':\n case 'dynamic-intercepted-(..)':\n return '(..)'\n case 'catchall-intercepted-(...)':\n case 'dynamic-intercepted-(...)':\n return '(...)'\n case 'catchall':\n case 'dynamic':\n case 'optional-catchall':\n default:\n return null\n }\n}\n"],"names":["interceptionPrefixFromParamType","paramType"],"mappings":"AAEA,OAAO,SAASA,gCACdC,SAA4B;IAE5B,OAAQA;QACN,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;QACL,KAAK;QACL;YACE,OAAO;IACX;AACF","ignoreList":[0]}
@@ -0,0 +1,83 @@
import { normalizeAppPath } from './app-paths';
// order matters here, the first match will be used
export const INTERCEPTION_ROUTE_MARKERS = [
'(..)(..)',
'(.)',
'(..)',
'(...)'
];
export function isInterceptionRouteAppPath(path) {
// TODO-APP: add more serious validation
return path.split('/').find((segment)=>INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m))) !== undefined;
}
export function extractInterceptionRouteInformation(path) {
let interceptingRoute;
let marker;
let interceptedRoute;
for (const segment of path.split('/')){
marker = INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m));
if (marker) {
;
[interceptingRoute, interceptedRoute] = path.split(marker, 2);
break;
}
}
if (!interceptingRoute || !marker || !interceptedRoute) {
throw Object.defineProperty(new Error(`Invalid interception route: ${path}. Must be in the format /<intercepting route>/(..|...|..)(..)/<intercepted route>`), "__NEXT_ERROR_CODE", {
value: "E269",
enumerable: false,
configurable: true
});
}
interceptingRoute = normalizeAppPath(interceptingRoute) // normalize the path, e.g. /(blog)/feed -> /feed
;
switch(marker){
case '(.)':
// (.) indicates that we should match with sibling routes, so we just need to append the intercepted route to the intercepting route
if (interceptingRoute === '/') {
interceptedRoute = `/${interceptedRoute}`;
} else {
interceptedRoute = interceptingRoute + '/' + interceptedRoute;
}
break;
case '(..)':
// (..) indicates that we should match at one level up, so we need to remove the last segment of the intercepting route
if (interceptingRoute === '/') {
throw Object.defineProperty(new Error(`Invalid interception route: ${path}. Cannot use (..) marker at the root level, use (.) instead.`), "__NEXT_ERROR_CODE", {
value: "E207",
enumerable: false,
configurable: true
});
}
interceptedRoute = interceptingRoute.split('/').slice(0, -1).concat(interceptedRoute).join('/');
break;
case '(...)':
// (...) will match the route segment in the root directory, so we need to use the root directory to prepend the intercepted route
interceptedRoute = '/' + interceptedRoute;
break;
case '(..)(..)':
// (..)(..) indicates that we should match at two levels up, so we need to remove the last two segments of the intercepting route
const splitInterceptingRoute = interceptingRoute.split('/');
if (splitInterceptingRoute.length <= 2) {
throw Object.defineProperty(new Error(`Invalid interception route: ${path}. Cannot use (..)(..) marker at the root level or one level up.`), "__NEXT_ERROR_CODE", {
value: "E486",
enumerable: false,
configurable: true
});
}
interceptedRoute = splitInterceptingRoute.slice(0, -2).concat(interceptedRoute).join('/');
break;
default:
throw Object.defineProperty(new Error('Invariant: unexpected marker'), "__NEXT_ERROR_CODE", {
value: "E112",
enumerable: false,
configurable: true
});
}
return {
interceptingRoute,
interceptedRoute
};
}
//# sourceMappingURL=interception-routes.js.map
File diff suppressed because one or more lines are too long
+43
View File
@@ -0,0 +1,43 @@
import { getRouteMatcher } from './route-matcher';
import { getRouteRegex } from './route-regex';
export function interpolateAs(route, asPathname, query) {
let interpolatedRoute = '';
const dynamicRegex = getRouteRegex(route);
const dynamicGroups = dynamicRegex.groups;
const dynamicMatches = // Try to match the dynamic route against the asPath
(asPathname !== route ? getRouteMatcher(dynamicRegex)(asPathname) : '') || // Fall back to reading the values from the href
// TODO: should this take priority; also need to change in the router.
query;
interpolatedRoute = route;
const params = Object.keys(dynamicGroups);
if (!params.every((param)=>{
let value = dynamicMatches[param] || '';
const { repeat, optional } = dynamicGroups[param];
// support single-level catch-all
// TODO: more robust handling for user-error (passing `/`)
let replaced = `[${repeat ? '...' : ''}${param}]`;
if (optional) {
replaced = `${!value ? '/' : ''}[${replaced}]`;
}
if (repeat && !Array.isArray(value)) value = [
value
];
return (optional || param in dynamicMatches) && // Interpolate group into data URL if present
(interpolatedRoute = interpolatedRoute.replace(replaced, repeat ? value.map(// these values should be fully encoded instead of just
// path delimiter escaped since they are being inserted
// into the URL and we expect URL encoded segments
// when parsing dynamic route params
(segment)=>encodeURIComponent(segment)).join('/') : encodeURIComponent(value)) || '/');
})) {
interpolatedRoute = '' // did not satisfy all requirements
;
// n.b. We ignore this error because we handle warning for this case in
// development in the `<Link>` component directly.
}
return {
params,
result: interpolatedRoute
};
}
//# sourceMappingURL=interpolate-as.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/interpolate-as.ts"],"sourcesContent":["import type { ParsedUrlQuery } from 'querystring'\n\nimport { getRouteMatcher } from './route-matcher'\nimport { getRouteRegex } from './route-regex'\n\nexport function interpolateAs(\n route: string,\n asPathname: string,\n query: ParsedUrlQuery\n) {\n let interpolatedRoute = ''\n\n const dynamicRegex = getRouteRegex(route)\n const dynamicGroups = dynamicRegex.groups\n const dynamicMatches =\n // Try to match the dynamic route against the asPath\n (asPathname !== route ? getRouteMatcher(dynamicRegex)(asPathname) : '') ||\n // Fall back to reading the values from the href\n // TODO: should this take priority; also need to change in the router.\n query\n\n interpolatedRoute = route\n const params = Object.keys(dynamicGroups)\n\n if (\n !params.every((param) => {\n let value = dynamicMatches[param] || ''\n const { repeat, optional } = dynamicGroups[param]\n\n // support single-level catch-all\n // TODO: more robust handling for user-error (passing `/`)\n let replaced = `[${repeat ? '...' : ''}${param}]`\n if (optional) {\n replaced = `${!value ? '/' : ''}[${replaced}]`\n }\n if (repeat && !Array.isArray(value)) value = [value]\n\n return (\n (optional || param in dynamicMatches) &&\n // Interpolate group into data URL if present\n (interpolatedRoute =\n interpolatedRoute!.replace(\n replaced,\n repeat\n ? (value as string[])\n .map(\n // these values should be fully encoded instead of just\n // path delimiter escaped since they are being inserted\n // into the URL and we expect URL encoded segments\n // when parsing dynamic route params\n (segment) => encodeURIComponent(segment)\n )\n .join('/')\n : encodeURIComponent(value as string)\n ) || '/')\n )\n })\n ) {\n interpolatedRoute = '' // did not satisfy all requirements\n\n // n.b. We ignore this error because we handle warning for this case in\n // development in the `<Link>` component directly.\n }\n return {\n params,\n result: interpolatedRoute,\n }\n}\n"],"names":["getRouteMatcher","getRouteRegex","interpolateAs","route","asPathname","query","interpolatedRoute","dynamicRegex","dynamicGroups","groups","dynamicMatches","params","Object","keys","every","param","value","repeat","optional","replaced","Array","isArray","replace","map","segment","encodeURIComponent","join","result"],"mappings":"AAEA,SAASA,eAAe,QAAQ,kBAAiB;AACjD,SAASC,aAAa,QAAQ,gBAAe;AAE7C,OAAO,SAASC,cACdC,KAAa,EACbC,UAAkB,EAClBC,KAAqB;IAErB,IAAIC,oBAAoB;IAExB,MAAMC,eAAeN,cAAcE;IACnC,MAAMK,gBAAgBD,aAAaE,MAAM;IACzC,MAAMC,iBAEJ,AADA,oDAAoD;IACnDN,CAAAA,eAAeD,QAAQH,gBAAgBO,cAAcH,cAAc,EAAC,KACrE,gDAAgD;IAChD,sEAAsE;IACtEC;IAEFC,oBAAoBH;IACpB,MAAMQ,SAASC,OAAOC,IAAI,CAACL;IAE3B,IACE,CAACG,OAAOG,KAAK,CAAC,CAACC;QACb,IAAIC,QAAQN,cAAc,CAACK,MAAM,IAAI;QACrC,MAAM,EAAEE,MAAM,EAAEC,QAAQ,EAAE,GAAGV,aAAa,CAACO,MAAM;QAEjD,iCAAiC;QACjC,0DAA0D;QAC1D,IAAII,WAAW,CAAC,CAAC,EAAEF,SAAS,QAAQ,KAAKF,MAAM,CAAC,CAAC;QACjD,IAAIG,UAAU;YACZC,WAAW,GAAG,CAACH,QAAQ,MAAM,GAAG,CAAC,EAAEG,SAAS,CAAC,CAAC;QAChD;QACA,IAAIF,UAAU,CAACG,MAAMC,OAAO,CAACL,QAAQA,QAAQ;YAACA;SAAM;QAEpD,OACE,AAACE,CAAAA,YAAYH,SAASL,cAAa,KACnC,6CAA6C;QAC5CJ,CAAAA,oBACCA,kBAAmBgB,OAAO,CACxBH,UACAF,SACI,AAACD,MACEO,GAAG,CACF,uDAAuD;QACvD,uDAAuD;QACvD,kDAAkD;QAClD,oCAAoC;QACpC,CAACC,UAAYC,mBAAmBD,UAEjCE,IAAI,CAAC,OACRD,mBAAmBT,WACpB,GAAE;IAEb,IACA;QACAV,oBAAoB,GAAG,mCAAmC;;IAE1D,uEAAuE;IACvE,kDAAkD;IACpD;IACA,OAAO;QACLK;QACAgB,QAAQrB;IACV;AACF","ignoreList":[0]}
+28
View File
@@ -0,0 +1,28 @@
import { HTML_LIMITED_BOT_UA_RE } from './html-bots';
// Bot crawler that will spin up a headless browser and execute JS.
// Only the main Googlebot search crawler executes JavaScript, not other Google crawlers.
// x-ref: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers
// This regex specifically matches "Googlebot" but NOT "Mediapartners-Google", "AdsBot-Google", etc.
const HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i;
export const HTML_LIMITED_BOT_UA_RE_STRING = HTML_LIMITED_BOT_UA_RE.source;
export { HTML_LIMITED_BOT_UA_RE };
function isDomBotUA(userAgent) {
return HEADLESS_BROWSER_BOT_UA_RE.test(userAgent);
}
function isHtmlLimitedBotUA(userAgent) {
return HTML_LIMITED_BOT_UA_RE.test(userAgent);
}
export function isBot(userAgent) {
return isDomBotUA(userAgent) || isHtmlLimitedBotUA(userAgent);
}
export function getBotType(userAgent) {
if (isDomBotUA(userAgent)) {
return 'dom';
}
if (isHtmlLimitedBotUA(userAgent)) {
return 'html';
}
return undefined;
}
//# sourceMappingURL=is-bot.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/is-bot.ts"],"sourcesContent":["import { HTML_LIMITED_BOT_UA_RE } from './html-bots'\n\n// Bot crawler that will spin up a headless browser and execute JS.\n// Only the main Googlebot search crawler executes JavaScript, not other Google crawlers.\n// x-ref: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers\n// This regex specifically matches \"Googlebot\" but NOT \"Mediapartners-Google\", \"AdsBot-Google\", etc.\nconst HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i\n\nexport const HTML_LIMITED_BOT_UA_RE_STRING = HTML_LIMITED_BOT_UA_RE.source\n\nexport { HTML_LIMITED_BOT_UA_RE }\n\nfunction isDomBotUA(userAgent: string) {\n return HEADLESS_BROWSER_BOT_UA_RE.test(userAgent)\n}\n\nfunction isHtmlLimitedBotUA(userAgent: string) {\n return HTML_LIMITED_BOT_UA_RE.test(userAgent)\n}\n\nexport function isBot(userAgent: string): boolean {\n return isDomBotUA(userAgent) || isHtmlLimitedBotUA(userAgent)\n}\n\nexport function getBotType(userAgent: string): 'dom' | 'html' | undefined {\n if (isDomBotUA(userAgent)) {\n return 'dom'\n }\n if (isHtmlLimitedBotUA(userAgent)) {\n return 'html'\n }\n return undefined\n}\n"],"names":["HTML_LIMITED_BOT_UA_RE","HEADLESS_BROWSER_BOT_UA_RE","HTML_LIMITED_BOT_UA_RE_STRING","source","isDomBotUA","userAgent","test","isHtmlLimitedBotUA","isBot","getBotType","undefined"],"mappings":"AAAA,SAASA,sBAAsB,QAAQ,cAAa;AAEpD,mEAAmE;AACnE,yFAAyF;AACzF,4FAA4F;AAC5F,oGAAoG;AACpG,MAAMC,6BAA6B;AAEnC,OAAO,MAAMC,gCAAgCF,uBAAuBG,MAAM,CAAA;AAE1E,SAASH,sBAAsB,GAAE;AAEjC,SAASI,WAAWC,SAAiB;IACnC,OAAOJ,2BAA2BK,IAAI,CAACD;AACzC;AAEA,SAASE,mBAAmBF,SAAiB;IAC3C,OAAOL,uBAAuBM,IAAI,CAACD;AACrC;AAEA,OAAO,SAASG,MAAMH,SAAiB;IACrC,OAAOD,WAAWC,cAAcE,mBAAmBF;AACrD;AAEA,OAAO,SAASI,WAAWJ,SAAiB;IAC1C,IAAID,WAAWC,YAAY;QACzB,OAAO;IACT;IACA,IAAIE,mBAAmBF,YAAY;QACjC,OAAO;IACT;IACA,OAAOK;AACT","ignoreList":[0]}
+22
View File
@@ -0,0 +1,22 @@
import { extractInterceptionRouteInformation, isInterceptionRouteAppPath } from './interception-routes';
// Identify /.*[param].*/ in route string
const TEST_ROUTE = /\/[^/]*\[[^/]+\][^/]*(?=\/|$)/;
// Identify /[param]/ in route string
const TEST_STRICT_ROUTE = /\/\[[^/]+\](?=\/|$)/;
/**
* Check if a route is dynamic.
*
* @param route - The route to check.
* @param strict - Whether to use strict mode which prohibits segments with prefixes/suffixes (default: true).
* @returns Whether the route is dynamic.
*/ export function isDynamicRoute(route, strict = true) {
if (isInterceptionRouteAppPath(route)) {
route = extractInterceptionRouteInformation(route).interceptedRoute;
}
if (strict) {
return TEST_STRICT_ROUTE.test(route);
}
return TEST_ROUTE.test(route);
}
//# sourceMappingURL=is-dynamic.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/is-dynamic.ts"],"sourcesContent":["import {\n extractInterceptionRouteInformation,\n isInterceptionRouteAppPath,\n} from './interception-routes'\n\n// Identify /.*[param].*/ in route string\nconst TEST_ROUTE = /\\/[^/]*\\[[^/]+\\][^/]*(?=\\/|$)/\n\n// Identify /[param]/ in route string\nconst TEST_STRICT_ROUTE = /\\/\\[[^/]+\\](?=\\/|$)/\n\n/**\n * Check if a route is dynamic.\n *\n * @param route - The route to check.\n * @param strict - Whether to use strict mode which prohibits segments with prefixes/suffixes (default: true).\n * @returns Whether the route is dynamic.\n */\nexport function isDynamicRoute(route: string, strict: boolean = true): boolean {\n if (isInterceptionRouteAppPath(route)) {\n route = extractInterceptionRouteInformation(route).interceptedRoute\n }\n\n if (strict) {\n return TEST_STRICT_ROUTE.test(route)\n }\n\n return TEST_ROUTE.test(route)\n}\n"],"names":["extractInterceptionRouteInformation","isInterceptionRouteAppPath","TEST_ROUTE","TEST_STRICT_ROUTE","isDynamicRoute","route","strict","interceptedRoute","test"],"mappings":"AAAA,SACEA,mCAAmC,EACnCC,0BAA0B,QACrB,wBAAuB;AAE9B,yCAAyC;AACzC,MAAMC,aAAa;AAEnB,qCAAqC;AACrC,MAAMC,oBAAoB;AAE1B;;;;;;CAMC,GACD,OAAO,SAASC,eAAeC,KAAa,EAAEC,SAAkB,IAAI;IAClE,IAAIL,2BAA2BI,QAAQ;QACrCA,QAAQL,oCAAoCK,OAAOE,gBAAgB;IACrE;IAEA,IAAID,QAAQ;QACV,OAAOH,kBAAkBK,IAAI,CAACH;IAChC;IAEA,OAAOH,WAAWM,IAAI,CAACH;AACzB","ignoreList":[0]}
+18
View File
@@ -0,0 +1,18 @@
import { isAbsoluteUrl, getLocationOrigin } from '../../utils';
import { hasBasePath } from '../../../../client/has-base-path';
/**
* Detects whether a given url is routable by the Next.js router (browser only).
*/ export function isLocalURL(url) {
// prevent a hydration mismatch on href for url with anchor refs
if (!isAbsoluteUrl(url)) return true;
try {
// absolute urls can be local if they are on the same origin
const locationOrigin = getLocationOrigin();
const resolved = new URL(url, locationOrigin);
return resolved.origin === locationOrigin && hasBasePath(resolved.pathname);
} catch (_) {
return false;
}
}
//# sourceMappingURL=is-local-url.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/is-local-url.ts"],"sourcesContent":["import { isAbsoluteUrl, getLocationOrigin } from '../../utils'\nimport { hasBasePath } from '../../../../client/has-base-path'\n\n/**\n * Detects whether a given url is routable by the Next.js router (browser only).\n */\nexport function isLocalURL(url: string): boolean {\n // prevent a hydration mismatch on href for url with anchor refs\n if (!isAbsoluteUrl(url)) return true\n try {\n // absolute urls can be local if they are on the same origin\n const locationOrigin = getLocationOrigin()\n const resolved = new URL(url, locationOrigin)\n return resolved.origin === locationOrigin && hasBasePath(resolved.pathname)\n } catch (_) {\n return false\n }\n}\n"],"names":["isAbsoluteUrl","getLocationOrigin","hasBasePath","isLocalURL","url","locationOrigin","resolved","URL","origin","pathname","_"],"mappings":"AAAA,SAASA,aAAa,EAAEC,iBAAiB,QAAQ,cAAa;AAC9D,SAASC,WAAW,QAAQ,mCAAkC;AAE9D;;CAEC,GACD,OAAO,SAASC,WAAWC,GAAW;IACpC,gEAAgE;IAChE,IAAI,CAACJ,cAAcI,MAAM,OAAO;IAChC,IAAI;QACF,4DAA4D;QAC5D,MAAMC,iBAAiBJ;QACvB,MAAMK,WAAW,IAAIC,IAAIH,KAAKC;QAC9B,OAAOC,SAASE,MAAM,KAAKH,kBAAkBH,YAAYI,SAASG,QAAQ;IAC5E,EAAE,OAAOC,GAAG;QACV,OAAO;IACT;AACF","ignoreList":[0]}
@@ -0,0 +1,21 @@
import { matchHas } from './prepare-destination';
export function getMiddlewareRouteMatcher(matchers) {
return (pathname, req, query)=>{
for (const matcher of matchers){
const routeMatch = new RegExp(matcher.regexp).exec(pathname);
if (!routeMatch) {
continue;
}
if (matcher.has || matcher.missing) {
const hasParams = matchHas(req, query, matcher.has, matcher.missing);
if (!hasParams) {
continue;
}
}
return true;
}
return false;
};
}
//# sourceMappingURL=middleware-route-matcher.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/middleware-route-matcher.ts"],"sourcesContent":["import type { BaseNextRequest } from '../../../../server/base-http'\nimport type { ProxyMatcher } from '../../../../build/analysis/get-page-static-info'\nimport type { Params } from '../../../../server/request/params'\nimport { matchHas } from './prepare-destination'\n\nexport interface MiddlewareRouteMatch {\n (\n pathname: string | null | undefined,\n request: BaseNextRequest,\n query: Params\n ): boolean\n}\n\nexport function getMiddlewareRouteMatcher(\n matchers: ProxyMatcher[]\n): MiddlewareRouteMatch {\n return (\n pathname: string | null | undefined,\n req: BaseNextRequest,\n query: Params\n ) => {\n for (const matcher of matchers) {\n const routeMatch = new RegExp(matcher.regexp).exec(pathname!)\n if (!routeMatch) {\n continue\n }\n\n if (matcher.has || matcher.missing) {\n const hasParams = matchHas(req, query, matcher.has, matcher.missing)\n if (!hasParams) {\n continue\n }\n }\n\n return true\n }\n\n return false\n }\n}\n"],"names":["matchHas","getMiddlewareRouteMatcher","matchers","pathname","req","query","matcher","routeMatch","RegExp","regexp","exec","has","missing","hasParams"],"mappings":"AAGA,SAASA,QAAQ,QAAQ,wBAAuB;AAUhD,OAAO,SAASC,0BACdC,QAAwB;IAExB,OAAO,CACLC,UACAC,KACAC;QAEA,KAAK,MAAMC,WAAWJ,SAAU;YAC9B,MAAMK,aAAa,IAAIC,OAAOF,QAAQG,MAAM,EAAEC,IAAI,CAACP;YACnD,IAAI,CAACI,YAAY;gBACf;YACF;YAEA,IAAID,QAAQK,GAAG,IAAIL,QAAQM,OAAO,EAAE;gBAClC,MAAMC,YAAYb,SAASI,KAAKC,OAAOC,QAAQK,GAAG,EAAEL,QAAQM,OAAO;gBACnE,IAAI,CAACC,WAAW;oBACd;gBACF;YACF;YAEA,OAAO;QACT;QAEA,OAAO;IACT;AACF","ignoreList":[0]}
+11
View File
@@ -0,0 +1,11 @@
export function omit(object, keys) {
const omitted = {};
Object.keys(object).forEach((key)=>{
if (!keys.includes(key)) {
omitted[key] = object[key];
}
});
return omitted;
}
//# sourceMappingURL=omit.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/omit.ts"],"sourcesContent":["export function omit<T extends { [key: string]: unknown }, K extends keyof T>(\n object: T,\n keys: K[]\n): Omit<T, K> {\n const omitted: { [key: string]: unknown } = {}\n Object.keys(object).forEach((key) => {\n if (!keys.includes(key as K)) {\n omitted[key] = object[key]\n }\n })\n return omitted as Omit<T, K>\n}\n"],"names":["omit","object","keys","omitted","Object","forEach","key","includes"],"mappings":"AAAA,OAAO,SAASA,KACdC,MAAS,EACTC,IAAS;IAET,MAAMC,UAAsC,CAAC;IAC7CC,OAAOF,IAAI,CAACD,QAAQI,OAAO,CAAC,CAACC;QAC3B,IAAI,CAACJ,KAAKK,QAAQ,CAACD,MAAW;YAC5BH,OAAO,CAACG,IAAI,GAAGL,MAAM,CAACK,IAAI;QAC5B;IACF;IACA,OAAOH;AACT","ignoreList":[0]}
@@ -0,0 +1,20 @@
import { DEFAULT_SEGMENT_KEY } from '../../segment';
export function parseLoaderTree(tree) {
const [segment, parallelRoutes, modules, staticSiblings] = tree;
const { layout, template } = modules;
let { page } = modules;
// a __DEFAULT__ segment means that this route didn't match any of the
// segments in the route, so we should use the default page
page = segment === DEFAULT_SEGMENT_KEY ? modules.defaultPage : page;
const conventionPath = layout?.[1] || template?.[1] || page?.[1];
return {
page,
segment,
modules,
/* it can be either layout / template / page */ conventionPath,
parallelRoutes,
staticSiblings
};
}
//# sourceMappingURL=parse-loader-tree.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/parse-loader-tree.ts"],"sourcesContent":["import { DEFAULT_SEGMENT_KEY } from '../../segment'\nimport type { LoaderTree } from '../../../../server/lib/app-dir-module'\n\nexport function parseLoaderTree(tree: LoaderTree) {\n const [segment, parallelRoutes, modules, staticSiblings] = tree\n const { layout, template } = modules\n let { page } = modules\n // a __DEFAULT__ segment means that this route didn't match any of the\n // segments in the route, so we should use the default page\n page = segment === DEFAULT_SEGMENT_KEY ? modules.defaultPage : page\n\n const conventionPath = layout?.[1] || template?.[1] || page?.[1]\n\n return {\n page,\n segment,\n modules,\n /* it can be either layout / template / page */\n conventionPath,\n parallelRoutes,\n staticSiblings,\n }\n}\n"],"names":["DEFAULT_SEGMENT_KEY","parseLoaderTree","tree","segment","parallelRoutes","modules","staticSiblings","layout","template","page","defaultPage","conventionPath"],"mappings":"AAAA,SAASA,mBAAmB,QAAQ,gBAAe;AAGnD,OAAO,SAASC,gBAAgBC,IAAgB;IAC9C,MAAM,CAACC,SAASC,gBAAgBC,SAASC,eAAe,GAAGJ;IAC3D,MAAM,EAAEK,MAAM,EAAEC,QAAQ,EAAE,GAAGH;IAC7B,IAAI,EAAEI,IAAI,EAAE,GAAGJ;IACf,sEAAsE;IACtE,2DAA2D;IAC3DI,OAAON,YAAYH,sBAAsBK,QAAQK,WAAW,GAAGD;IAE/D,MAAME,iBAAiBJ,QAAQ,CAAC,EAAE,IAAIC,UAAU,CAAC,EAAE,IAAIC,MAAM,CAAC,EAAE;IAEhE,OAAO;QACLA;QACAN;QACAE;QACA,6CAA6C,GAC7CM;QACAP;QACAE;IACF;AACF","ignoreList":[0]}
+23
View File
@@ -0,0 +1,23 @@
/**
* Given a path this function will find the pathname, query and hash and return
* them. This is useful to parse full paths on the client side.
* @param path A path to parse e.g. /foo/bar?id=1#hash
*/ export function parsePath(path) {
const hashIndex = path.indexOf('#');
const queryIndex = path.indexOf('?');
const hasQuery = queryIndex > -1 && (hashIndex < 0 || queryIndex < hashIndex);
if (hasQuery || hashIndex > -1) {
return {
pathname: path.substring(0, hasQuery ? queryIndex : hashIndex),
query: hasQuery ? path.substring(queryIndex, hashIndex > -1 ? hashIndex : undefined) : '',
hash: hashIndex > -1 ? path.slice(hashIndex) : ''
};
}
return {
pathname: path,
query: '',
hash: ''
};
}
//# sourceMappingURL=parse-path.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/parse-path.ts"],"sourcesContent":["/**\n * Given a path this function will find the pathname, query and hash and return\n * them. This is useful to parse full paths on the client side.\n * @param path A path to parse e.g. /foo/bar?id=1#hash\n */\nexport function parsePath(path: string) {\n const hashIndex = path.indexOf('#')\n const queryIndex = path.indexOf('?')\n const hasQuery = queryIndex > -1 && (hashIndex < 0 || queryIndex < hashIndex)\n\n if (hasQuery || hashIndex > -1) {\n return {\n pathname: path.substring(0, hasQuery ? queryIndex : hashIndex),\n query: hasQuery\n ? path.substring(queryIndex, hashIndex > -1 ? hashIndex : undefined)\n : '',\n hash: hashIndex > -1 ? path.slice(hashIndex) : '',\n }\n }\n\n return { pathname: path, query: '', hash: '' }\n}\n"],"names":["parsePath","path","hashIndex","indexOf","queryIndex","hasQuery","pathname","substring","query","undefined","hash","slice"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,SAASA,UAAUC,IAAY;IACpC,MAAMC,YAAYD,KAAKE,OAAO,CAAC;IAC/B,MAAMC,aAAaH,KAAKE,OAAO,CAAC;IAChC,MAAME,WAAWD,aAAa,CAAC,KAAMF,CAAAA,YAAY,KAAKE,aAAaF,SAAQ;IAE3E,IAAIG,YAAYH,YAAY,CAAC,GAAG;QAC9B,OAAO;YACLI,UAAUL,KAAKM,SAAS,CAAC,GAAGF,WAAWD,aAAaF;YACpDM,OAAOH,WACHJ,KAAKM,SAAS,CAACH,YAAYF,YAAY,CAAC,IAAIA,YAAYO,aACxD;YACJC,MAAMR,YAAY,CAAC,IAAID,KAAKU,KAAK,CAACT,aAAa;QACjD;IACF;IAEA,OAAO;QAAEI,UAAUL;QAAMO,OAAO;QAAIE,MAAM;IAAG;AAC/C","ignoreList":[0]}
@@ -0,0 +1,33 @@
import { getLocationOrigin } from '../../utils';
import { searchParamsToUrlQuery } from './querystring';
export function parseRelativeUrl(url, base, parseQuery = true) {
const globalBase = new URL(typeof window === 'undefined' ? 'http://n' : getLocationOrigin());
const resolvedBase = base ? new URL(base, globalBase) : url.startsWith('.') ? new URL(typeof window === 'undefined' ? 'http://n' : window.location.href) : globalBase;
const { pathname, searchParams, search, hash, href, origin } = url.startsWith('/') ? // See https://nodejs.org/api/http.html#messageurl
// Not using `origin` to support other protocols
new URL(`${resolvedBase.protocol}//${resolvedBase.host}${url}`) : new URL(url, resolvedBase);
if (origin !== globalBase.origin) {
throw Object.defineProperty(new Error(`invariant: invalid relative URL, router received ${url}`), "__NEXT_ERROR_CODE", {
value: "E159",
enumerable: false,
configurable: true
});
}
return {
auth: null,
host: null,
hostname: null,
pathname,
port: null,
protocol: null,
query: parseQuery ? searchParamsToUrlQuery(searchParams) : undefined,
search,
hash,
href: href.slice(origin.length),
// We don't know for relative URLs at this point since we set a custom, internal
// base that isn't surfaced to users.
slashes: null
};
}
//# sourceMappingURL=parse-relative-url.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/parse-relative-url.ts"],"sourcesContent":["import type { ParsedUrlQuery } from 'querystring'\nimport { getLocationOrigin } from '../../utils'\nimport { searchParamsToUrlQuery } from './querystring'\n\nexport interface ParsedRelativeUrl {\n auth: string | null\n hash: string\n host: string | null\n hostname: string | null\n href: string\n pathname: string\n port: string | null\n protocol: string | null\n query: ParsedUrlQuery\n search: string\n slashes: null\n}\n\n/**\n * Parses path-relative urls (e.g. `/hello/world?foo=bar`). If url isn't path-relative\n * (e.g. `./hello`) then at least base must be.\n * Absolute urls are rejected with one exception, in the browser, absolute urls that are on\n * the current origin will be parsed as relative\n */\nexport function parseRelativeUrl(\n url: string,\n base?: string,\n parseQuery?: true\n): ParsedRelativeUrl\nexport function parseRelativeUrl(\n url: string,\n base: string | undefined,\n parseQuery: false\n): Omit<ParsedRelativeUrl, 'query'>\nexport function parseRelativeUrl(\n url: string,\n base?: string,\n parseQuery = true\n): ParsedRelativeUrl | Omit<ParsedRelativeUrl, 'query'> {\n const globalBase = new URL(\n typeof window === 'undefined' ? 'http://n' : getLocationOrigin()\n )\n\n const resolvedBase = base\n ? new URL(base, globalBase)\n : url.startsWith('.')\n ? new URL(\n typeof window === 'undefined' ? 'http://n' : window.location.href\n )\n : globalBase\n\n const { pathname, searchParams, search, hash, href, origin } = url.startsWith(\n '/'\n )\n ? // 'http://localhost:3000///' would be received as '///' in Node.js' IncomingMessage\n // See https://nodejs.org/api/http.html#messageurl\n // Not using `origin` to support other protocols\n new URL(`${resolvedBase.protocol}//${resolvedBase.host}${url}`)\n : new URL(url, resolvedBase)\n\n if (origin !== globalBase.origin) {\n throw new Error(`invariant: invalid relative URL, router received ${url}`)\n }\n\n return {\n auth: null,\n host: null,\n hostname: null,\n pathname,\n port: null,\n protocol: null,\n query: parseQuery ? searchParamsToUrlQuery(searchParams) : undefined,\n search,\n hash,\n href: href.slice(origin.length),\n // We don't know for relative URLs at this point since we set a custom, internal\n // base that isn't surfaced to users.\n slashes: null,\n }\n}\n"],"names":["getLocationOrigin","searchParamsToUrlQuery","parseRelativeUrl","url","base","parseQuery","globalBase","URL","window","resolvedBase","startsWith","location","href","pathname","searchParams","search","hash","origin","protocol","host","Error","auth","hostname","port","query","undefined","slice","length","slashes"],"mappings":"AACA,SAASA,iBAAiB,QAAQ,cAAa;AAC/C,SAASC,sBAAsB,QAAQ,gBAAe;AAgCtD,OAAO,SAASC,iBACdC,GAAW,EACXC,IAAa,EACbC,aAAa,IAAI;IAEjB,MAAMC,aAAa,IAAIC,IACrB,OAAOC,WAAW,cAAc,aAAaR;IAG/C,MAAMS,eAAeL,OACjB,IAAIG,IAAIH,MAAME,cACdH,IAAIO,UAAU,CAAC,OACb,IAAIH,IACF,OAAOC,WAAW,cAAc,aAAaA,OAAOG,QAAQ,CAACC,IAAI,IAEnEN;IAEN,MAAM,EAAEO,QAAQ,EAAEC,YAAY,EAAEC,MAAM,EAAEC,IAAI,EAAEJ,IAAI,EAAEK,MAAM,EAAE,GAAGd,IAAIO,UAAU,CAC3E,OAGE,kDAAkD;IAClD,gDAAgD;IAChD,IAAIH,IAAI,GAAGE,aAAaS,QAAQ,CAAC,EAAE,EAAET,aAAaU,IAAI,GAAGhB,KAAK,IAC9D,IAAII,IAAIJ,KAAKM;IAEjB,IAAIQ,WAAWX,WAAWW,MAAM,EAAE;QAChC,MAAM,qBAAoE,CAApE,IAAIG,MAAM,CAAC,iDAAiD,EAAEjB,KAAK,GAAnE,qBAAA;mBAAA;wBAAA;0BAAA;QAAmE;IAC3E;IAEA,OAAO;QACLkB,MAAM;QACNF,MAAM;QACNG,UAAU;QACVT;QACAU,MAAM;QACNL,UAAU;QACVM,OAAOnB,aAAaJ,uBAAuBa,gBAAgBW;QAC3DV;QACAC;QACAJ,MAAMA,KAAKc,KAAK,CAACT,OAAOU,MAAM;QAC9B,gFAAgF;QAChF,qCAAqC;QACrCC,SAAS;IACX;AACF","ignoreList":[0]}
+28
View File
@@ -0,0 +1,28 @@
import { searchParamsToUrlQuery } from './querystring';
import { parseRelativeUrl } from './parse-relative-url';
export function parseUrl(url) {
if (url.startsWith('/')) {
return parseRelativeUrl(url);
}
const parsedURL = new URL(url);
const username = parsedURL.username;
const password = parsedURL.password;
const auth = username ? password ? `${username}:${password}` : username : null;
const pathname = parsedURL.pathname;
const search = parsedURL.search;
return {
auth,
hash: parsedURL.hash,
hostname: parsedURL.hostname,
href: parsedURL.href,
pathname,
port: parsedURL.port,
protocol: parsedURL.protocol,
query: searchParamsToUrlQuery(parsedURL.searchParams),
search,
origin: parsedURL.origin,
slashes: parsedURL.href.slice(parsedURL.protocol.length, parsedURL.protocol.length + 2) === '//'
};
}
//# sourceMappingURL=parse-url.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/parse-url.ts"],"sourcesContent":["import type { ParsedUrlQuery } from 'querystring'\n\nimport { searchParamsToUrlQuery } from './querystring'\nimport { parseRelativeUrl } from './parse-relative-url'\n\nexport interface ParsedUrl {\n auth: string | null\n hash: string\n hostname: string | null\n href: string\n origin?: string | null\n pathname: string\n port: string | null\n protocol: string | null\n query: ParsedUrlQuery\n search: string\n slashes: boolean | null\n}\n\nexport function parseUrl(url: string): ParsedUrl {\n if (url.startsWith('/')) {\n return parseRelativeUrl(url)\n }\n\n const parsedURL = new URL(url)\n const username = parsedURL.username\n const password = parsedURL.password\n const auth = username\n ? password\n ? `${username}:${password}`\n : username\n : null\n const pathname = parsedURL.pathname\n const search = parsedURL.search\n return {\n auth,\n hash: parsedURL.hash,\n hostname: parsedURL.hostname,\n href: parsedURL.href,\n pathname,\n port: parsedURL.port,\n protocol: parsedURL.protocol,\n query: searchParamsToUrlQuery(parsedURL.searchParams),\n search,\n origin: parsedURL.origin,\n slashes:\n parsedURL.href.slice(\n parsedURL.protocol.length,\n parsedURL.protocol.length + 2\n ) === '//',\n }\n}\n"],"names":["searchParamsToUrlQuery","parseRelativeUrl","parseUrl","url","startsWith","parsedURL","URL","username","password","auth","pathname","search","hash","hostname","href","port","protocol","query","searchParams","origin","slashes","slice","length"],"mappings":"AAEA,SAASA,sBAAsB,QAAQ,gBAAe;AACtD,SAASC,gBAAgB,QAAQ,uBAAsB;AAgBvD,OAAO,SAASC,SAASC,GAAW;IAClC,IAAIA,IAAIC,UAAU,CAAC,MAAM;QACvB,OAAOH,iBAAiBE;IAC1B;IAEA,MAAME,YAAY,IAAIC,IAAIH;IAC1B,MAAMI,WAAWF,UAAUE,QAAQ;IACnC,MAAMC,WAAWH,UAAUG,QAAQ;IACnC,MAAMC,OAAOF,WACTC,WACE,GAAGD,SAAS,CAAC,EAAEC,UAAU,GACzBD,WACF;IACJ,MAAMG,WAAWL,UAAUK,QAAQ;IACnC,MAAMC,SAASN,UAAUM,MAAM;IAC/B,OAAO;QACLF;QACAG,MAAMP,UAAUO,IAAI;QACpBC,UAAUR,UAAUQ,QAAQ;QAC5BC,MAAMT,UAAUS,IAAI;QACpBJ;QACAK,MAAMV,UAAUU,IAAI;QACpBC,UAAUX,UAAUW,QAAQ;QAC5BC,OAAOjB,uBAAuBK,UAAUa,YAAY;QACpDP;QACAQ,QAAQd,UAAUc,MAAM;QACxBC,SACEf,UAAUS,IAAI,CAACO,KAAK,CAClBhB,UAAUW,QAAQ,CAACM,MAAM,EACzBjB,UAAUW,QAAQ,CAACM,MAAM,GAAG,OACxB;IACV;AACF","ignoreList":[0]}
+16
View File
@@ -0,0 +1,16 @@
import { parsePath } from './parse-path';
/**
* Checks if a given path starts with a given prefix. It ensures it matches
* exactly without containing extra chars. e.g. prefix /docs should replace
* for /docs, /docs/, /docs/a but not /docsss
* @param path The path to check.
* @param prefix The prefix to check against.
*/ export function pathHasPrefix(path, prefix) {
if (typeof path !== 'string') {
return false;
}
const { pathname } = parsePath(path);
return pathname === prefix || pathname.startsWith(prefix + '/');
}
//# sourceMappingURL=path-has-prefix.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/path-has-prefix.ts"],"sourcesContent":["import { parsePath } from './parse-path'\n\n/**\n * Checks if a given path starts with a given prefix. It ensures it matches\n * exactly without containing extra chars. e.g. prefix /docs should replace\n * for /docs, /docs/, /docs/a but not /docsss\n * @param path The path to check.\n * @param prefix The prefix to check against.\n */\nexport function pathHasPrefix(path: string, prefix: string) {\n if (typeof path !== 'string') {\n return false\n }\n\n const { pathname } = parsePath(path)\n return pathname === prefix || pathname.startsWith(prefix + '/')\n}\n"],"names":["parsePath","pathHasPrefix","path","prefix","pathname","startsWith"],"mappings":"AAAA,SAASA,SAAS,QAAQ,eAAc;AAExC;;;;;;CAMC,GACD,OAAO,SAASC,cAAcC,IAAY,EAAEC,MAAc;IACxD,IAAI,OAAOD,SAAS,UAAU;QAC5B,OAAO;IACT;IAEA,MAAM,EAAEE,QAAQ,EAAE,GAAGJ,UAAUE;IAC/B,OAAOE,aAAaD,UAAUC,SAASC,UAAU,CAACF,SAAS;AAC7D","ignoreList":[0]}
+44
View File
@@ -0,0 +1,44 @@
import { regexpToFunction } from 'next/dist/compiled/path-to-regexp';
import { pathToRegexp } from 'next/dist/compiled/path-to-regexp';
/**
* Generates a path matcher function for a given path and options based on
* path-to-regexp. By default the match will be case insensitive, non strict
* and delimited by `/`.
*/ export function getPathMatch(path, options) {
const keys = [];
const regexp = pathToRegexp(path, keys, {
delimiter: '/',
sensitive: typeof options?.sensitive === 'boolean' ? options.sensitive : false,
strict: options?.strict
});
const matcher = regexpToFunction(options?.regexModifier ? new RegExp(options.regexModifier(regexp.source), regexp.flags) : regexp, keys);
/**
* A matcher function that will check if a given pathname matches the path
* given in the builder function. When the path does not match it will return
* `false` but if it does it will return an object with the matched params
* merged with the params provided in the second argument.
*/ return (pathname, params)=>{
// If no pathname is provided it's not a match.
if (typeof pathname !== 'string') return false;
const match = matcher(pathname);
// If the path did not match `false` will be returned.
if (!match) return false;
/**
* If unnamed params are not allowed they must be removed from
* the matched parameters. path-to-regexp uses "string" for named and
* "number" for unnamed parameters.
*/ if (options?.removeUnnamedParams) {
for (const key of keys){
if (typeof key.name === 'number') {
delete match.params[key.name];
}
}
}
return {
...params,
...match.params
};
};
}
//# sourceMappingURL=path-match.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/path-match.ts"],"sourcesContent":["import type { Key } from 'next/dist/compiled/path-to-regexp'\nimport { regexpToFunction } from 'next/dist/compiled/path-to-regexp'\nimport { pathToRegexp } from 'next/dist/compiled/path-to-regexp'\n\ninterface Options {\n /**\n * A transformer function that will be applied to the regexp generated\n * from the provided path and path-to-regexp.\n */\n regexModifier?: (regex: string) => string\n /**\n * When true the function will remove all unnamed parameters\n * from the matched parameters.\n */\n removeUnnamedParams?: boolean\n /**\n * When true the regexp won't allow an optional trailing delimiter\n * to match.\n */\n strict?: boolean\n\n /**\n * When true the matcher will be case-sensitive, defaults to false\n */\n sensitive?: boolean\n}\n\nexport type PatchMatcher = (\n pathname: string,\n params?: Record<string, any>\n) => Record<string, any> | false\n\n/**\n * Generates a path matcher function for a given path and options based on\n * path-to-regexp. By default the match will be case insensitive, non strict\n * and delimited by `/`.\n */\nexport function getPathMatch(path: string, options?: Options): PatchMatcher {\n const keys: Key[] = []\n const regexp = pathToRegexp(path, keys, {\n delimiter: '/',\n sensitive:\n typeof options?.sensitive === 'boolean' ? options.sensitive : false,\n strict: options?.strict,\n })\n\n const matcher = regexpToFunction<Record<string, any>>(\n options?.regexModifier\n ? new RegExp(options.regexModifier(regexp.source), regexp.flags)\n : regexp,\n keys\n )\n\n /**\n * A matcher function that will check if a given pathname matches the path\n * given in the builder function. When the path does not match it will return\n * `false` but if it does it will return an object with the matched params\n * merged with the params provided in the second argument.\n */\n return (pathname, params) => {\n // If no pathname is provided it's not a match.\n if (typeof pathname !== 'string') return false\n\n const match = matcher(pathname)\n\n // If the path did not match `false` will be returned.\n if (!match) return false\n\n /**\n * If unnamed params are not allowed they must be removed from\n * the matched parameters. path-to-regexp uses \"string\" for named and\n * \"number\" for unnamed parameters.\n */\n if (options?.removeUnnamedParams) {\n for (const key of keys) {\n if (typeof key.name === 'number') {\n delete match.params[key.name]\n }\n }\n }\n\n return { ...params, ...match.params }\n }\n}\n"],"names":["regexpToFunction","pathToRegexp","getPathMatch","path","options","keys","regexp","delimiter","sensitive","strict","matcher","regexModifier","RegExp","source","flags","pathname","params","match","removeUnnamedParams","key","name"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,oCAAmC;AACpE,SAASC,YAAY,QAAQ,oCAAmC;AA8BhE;;;;CAIC,GACD,OAAO,SAASC,aAAaC,IAAY,EAAEC,OAAiB;IAC1D,MAAMC,OAAc,EAAE;IACtB,MAAMC,SAASL,aAAaE,MAAME,MAAM;QACtCE,WAAW;QACXC,WACE,OAAOJ,SAASI,cAAc,YAAYJ,QAAQI,SAAS,GAAG;QAChEC,QAAQL,SAASK;IACnB;IAEA,MAAMC,UAAUV,iBACdI,SAASO,gBACL,IAAIC,OAAOR,QAAQO,aAAa,CAACL,OAAOO,MAAM,GAAGP,OAAOQ,KAAK,IAC7DR,QACJD;IAGF;;;;;GAKC,GACD,OAAO,CAACU,UAAUC;QAChB,+CAA+C;QAC/C,IAAI,OAAOD,aAAa,UAAU,OAAO;QAEzC,MAAME,QAAQP,QAAQK;QAEtB,sDAAsD;QACtD,IAAI,CAACE,OAAO,OAAO;QAEnB;;;;KAIC,GACD,IAAIb,SAASc,qBAAqB;YAChC,KAAK,MAAMC,OAAOd,KAAM;gBACtB,IAAI,OAAOc,IAAIC,IAAI,KAAK,UAAU;oBAChC,OAAOH,MAAMD,MAAM,CAACG,IAAIC,IAAI,CAAC;gBAC/B;YACF;QACF;QAEA,OAAO;YAAE,GAAGJ,MAAM;YAAE,GAAGC,MAAMD,MAAM;QAAC;IACtC;AACF","ignoreList":[0]}
@@ -0,0 +1,262 @@
import { escapeStringRegexp } from '../../escape-regexp';
import { parseUrl } from './parse-url';
import { INTERCEPTION_ROUTE_MARKERS, isInterceptionRouteAppPath } from './interception-routes';
import { getCookieParser } from '../../../../server/api-utils/get-cookie-parser';
import { safePathToRegexp, safeCompile } from './route-match-utils';
/**
* Ensure only a-zA-Z are used for param names for proper interpolating
* with path-to-regexp
*/ function getSafeParamName(paramName) {
let newParamName = '';
for(let i = 0; i < paramName.length; i++){
const charCode = paramName.charCodeAt(i);
if (charCode > 64 && charCode < 91 || // A-Z
charCode > 96 && charCode < 123 // a-z
) {
newParamName += paramName[i];
}
}
return newParamName;
}
function escapeSegment(str, segmentName) {
return str.replace(new RegExp(`:${escapeStringRegexp(segmentName)}`, 'g'), `__ESC_COLON_${segmentName}`);
}
function unescapeSegments(str) {
return str.replace(/__ESC_COLON_/gi, ':');
}
export function matchHas(req, query, has = [], missing = []) {
const params = {};
const hasMatch = (hasItem)=>{
let value;
let key = hasItem.key;
switch(hasItem.type){
case 'header':
{
key = key.toLowerCase();
value = req.headers[key];
break;
}
case 'cookie':
{
if ('cookies' in req) {
value = req.cookies[hasItem.key];
} else {
const cookies = getCookieParser(req.headers)();
value = cookies[hasItem.key];
}
break;
}
case 'query':
{
value = query[key];
break;
}
case 'host':
{
const { host } = req?.headers || {};
// remove port from host if present
const hostname = host?.split(':', 1)[0].toLowerCase();
value = hostname;
break;
}
default:
{
break;
}
}
if (!hasItem.value && value) {
params[getSafeParamName(key)] = value;
return true;
} else if (value) {
const matcher = new RegExp(`^${hasItem.value}$`);
const matches = Array.isArray(value) ? value.slice(-1)[0].match(matcher) : value.match(matcher);
if (matches) {
if (Array.isArray(matches)) {
if (matches.groups) {
Object.keys(matches.groups).forEach((groupKey)=>{
params[groupKey] = matches.groups[groupKey];
});
} else if (hasItem.type === 'host' && matches[0]) {
params.host = matches[0];
}
}
return true;
}
}
return false;
};
const allMatch = has.every((item)=>hasMatch(item)) && !missing.some((item)=>hasMatch(item));
if (allMatch) {
return params;
}
return false;
}
export function compileNonPath(value, params) {
if (!value.includes(':')) {
return value;
}
for (const key of Object.keys(params)){
if (value.includes(`:${key}`)) {
value = value.replace(new RegExp(`:${key}\\*`, 'g'), `:${key}--ESCAPED_PARAM_ASTERISKS`).replace(new RegExp(`:${key}\\?`, 'g'), `:${key}--ESCAPED_PARAM_QUESTION`).replace(new RegExp(`:${key}\\+`, 'g'), `:${key}--ESCAPED_PARAM_PLUS`).replace(new RegExp(`:${key}(?!\\w)`, 'g'), `--ESCAPED_PARAM_COLON${key}`);
}
}
value = value.replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1').replace(/--ESCAPED_PARAM_PLUS/g, '+').replace(/--ESCAPED_PARAM_COLON/g, ':').replace(/--ESCAPED_PARAM_QUESTION/g, '?').replace(/--ESCAPED_PARAM_ASTERISKS/g, '*');
// the value needs to start with a forward-slash to be compiled
// correctly
return safeCompile(`/${value}`, {
validate: false
})(params).slice(1);
}
export function parseDestination(args) {
let escaped = args.destination;
for (const param of Object.keys({
...args.params,
...args.query
})){
if (!param) continue;
escaped = escapeSegment(escaped, param);
}
const parsed = parseUrl(escaped);
let pathname = parsed.pathname;
if (pathname) {
pathname = unescapeSegments(pathname);
}
let href = parsed.href;
if (href) {
href = unescapeSegments(href);
}
let hostname = parsed.hostname;
if (hostname) {
hostname = unescapeSegments(hostname);
}
let hash = parsed.hash;
if (hash) {
hash = unescapeSegments(hash);
}
let search = parsed.search;
if (search) {
search = unescapeSegments(search);
}
let origin = parsed.origin;
if (origin) {
origin = unescapeSegments(origin);
}
return {
...parsed,
pathname,
hostname,
href,
hash,
search,
origin
};
}
export function prepareDestination(args) {
const parsedDestination = parseDestination(args);
const { hostname: destHostname, query: destQuery, search: destSearch } = parsedDestination;
// The following code assumes that the pathname here includes the hash if it's
// present.
let destPath = parsedDestination.pathname;
if (parsedDestination.hash) {
destPath = `${destPath}${parsedDestination.hash}`;
}
const destParams = [];
const destPathParamKeys = [];
safePathToRegexp(destPath, destPathParamKeys);
for (const key of destPathParamKeys){
destParams.push(key.name);
}
if (destHostname) {
const destHostnameParamKeys = [];
safePathToRegexp(destHostname, destHostnameParamKeys);
for (const key of destHostnameParamKeys){
destParams.push(key.name);
}
}
const destPathCompiler = safeCompile(destPath, // we don't validate while compiling the destination since we should
// have already validated before we got to this point and validating
// breaks compiling destinations with named pattern params from the source
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
// since compile validation is meant for reversing and not for inserting
// params from a separate path-regex into another
{
validate: false
});
let destHostnameCompiler;
if (destHostname) {
destHostnameCompiler = safeCompile(destHostname, {
validate: false
});
}
// update any params in query values
for (const [key, strOrArray] of Object.entries(destQuery)){
// the value needs to start with a forward-slash to be compiled
// correctly
if (Array.isArray(strOrArray)) {
destQuery[key] = strOrArray.map((value)=>compileNonPath(unescapeSegments(value), args.params));
} else if (typeof strOrArray === 'string') {
destQuery[key] = compileNonPath(unescapeSegments(strOrArray), args.params);
}
}
// add path params to query if it's not a redirect and not
// already defined in destination query or path
let paramKeys = Object.keys(args.params).filter((name)=>name !== 'nextInternalLocale');
if (args.appendParamsToQuery && !paramKeys.some((key)=>destParams.includes(key))) {
for (const key of paramKeys){
if (!(key in destQuery)) {
destQuery[key] = args.params[key];
}
}
}
let newUrl;
// The compiler also that the interception route marker is an unnamed param, hence '0',
// so we need to add it to the params object.
if (isInterceptionRouteAppPath(destPath)) {
for (const segment of destPath.split('/')){
const marker = INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m));
if (marker) {
if (marker === '(..)(..)') {
args.params['0'] = '(..)';
args.params['1'] = '(..)';
} else {
args.params['0'] = marker;
}
break;
}
}
}
try {
newUrl = destPathCompiler(args.params);
const [pathname, hash] = newUrl.split('#', 2);
if (destHostnameCompiler) {
parsedDestination.hostname = destHostnameCompiler(args.params);
}
parsedDestination.pathname = pathname;
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`;
parsedDestination.search = destSearch ? compileNonPath(destSearch, args.params) : '';
} catch (err) {
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
throw Object.defineProperty(new Error(`To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://nextjs.org/docs/messages/invalid-multi-match`), "__NEXT_ERROR_CODE", {
value: "E329",
enumerable: false,
configurable: true
});
}
throw err;
}
// Query merge order lowest priority to highest
// 1. initial URL query values
// 2. path segment values
// 3. destination specified query values
parsedDestination.query = {
...args.query,
...parsedDestination.query
};
return {
newUrl,
destQuery,
parsedDestination
};
}
//# sourceMappingURL=prepare-destination.js.map
File diff suppressed because one or more lines are too long
+53
View File
@@ -0,0 +1,53 @@
export function searchParamsToUrlQuery(searchParams) {
const query = {};
for (const [key, value] of searchParams.entries()){
const existing = query[key];
if (typeof existing === 'undefined') {
query[key] = value;
} else if (Array.isArray(existing)) {
existing.push(value);
} else {
query[key] = [
existing,
value
];
}
}
return query;
}
function stringifyUrlQueryParam(param) {
if (typeof param === 'string') {
return param;
}
if (typeof param === 'number' && !isNaN(param) || typeof param === 'boolean') {
return String(param);
} else {
return '';
}
}
export function urlQueryToSearchParams(query) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(query)){
if (Array.isArray(value)) {
for (const item of value){
searchParams.append(key, stringifyUrlQueryParam(item));
}
} else {
searchParams.set(key, stringifyUrlQueryParam(value));
}
}
return searchParams;
}
export function assign(target, ...searchParamsList) {
for (const searchParams of searchParamsList){
for (const key of searchParams.keys()){
target.delete(key);
}
for (const [key, value] of searchParams.entries()){
target.append(key, value);
}
}
return target;
}
//# sourceMappingURL=querystring.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/querystring.ts"],"sourcesContent":["import type { ParsedUrlQuery } from 'querystring'\n\nexport function searchParamsToUrlQuery(\n searchParams: URLSearchParams\n): ParsedUrlQuery {\n const query: ParsedUrlQuery = {}\n for (const [key, value] of searchParams.entries()) {\n const existing = query[key]\n if (typeof existing === 'undefined') {\n query[key] = value\n } else if (Array.isArray(existing)) {\n existing.push(value)\n } else {\n query[key] = [existing, value]\n }\n }\n return query\n}\n\nfunction stringifyUrlQueryParam(param: unknown): string {\n if (typeof param === 'string') {\n return param\n }\n\n if (\n (typeof param === 'number' && !isNaN(param)) ||\n typeof param === 'boolean'\n ) {\n return String(param)\n } else {\n return ''\n }\n}\n\nexport function urlQueryToSearchParams(query: ParsedUrlQuery): URLSearchParams {\n const searchParams = new URLSearchParams()\n for (const [key, value] of Object.entries(query)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n searchParams.append(key, stringifyUrlQueryParam(item))\n }\n } else {\n searchParams.set(key, stringifyUrlQueryParam(value))\n }\n }\n return searchParams\n}\n\nexport function assign(\n target: URLSearchParams,\n ...searchParamsList: URLSearchParams[]\n): URLSearchParams {\n for (const searchParams of searchParamsList) {\n for (const key of searchParams.keys()) {\n target.delete(key)\n }\n\n for (const [key, value] of searchParams.entries()) {\n target.append(key, value)\n }\n }\n\n return target\n}\n"],"names":["searchParamsToUrlQuery","searchParams","query","key","value","entries","existing","Array","isArray","push","stringifyUrlQueryParam","param","isNaN","String","urlQueryToSearchParams","URLSearchParams","Object","item","append","set","assign","target","searchParamsList","keys","delete"],"mappings":"AAEA,OAAO,SAASA,uBACdC,YAA6B;IAE7B,MAAMC,QAAwB,CAAC;IAC/B,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIH,aAAaI,OAAO,GAAI;QACjD,MAAMC,WAAWJ,KAAK,CAACC,IAAI;QAC3B,IAAI,OAAOG,aAAa,aAAa;YACnCJ,KAAK,CAACC,IAAI,GAAGC;QACf,OAAO,IAAIG,MAAMC,OAAO,CAACF,WAAW;YAClCA,SAASG,IAAI,CAACL;QAChB,OAAO;YACLF,KAAK,CAACC,IAAI,GAAG;gBAACG;gBAAUF;aAAM;QAChC;IACF;IACA,OAAOF;AACT;AAEA,SAASQ,uBAAuBC,KAAc;IAC5C,IAAI,OAAOA,UAAU,UAAU;QAC7B,OAAOA;IACT;IAEA,IACE,AAAC,OAAOA,UAAU,YAAY,CAACC,MAAMD,UACrC,OAAOA,UAAU,WACjB;QACA,OAAOE,OAAOF;IAChB,OAAO;QACL,OAAO;IACT;AACF;AAEA,OAAO,SAASG,uBAAuBZ,KAAqB;IAC1D,MAAMD,eAAe,IAAIc;IACzB,KAAK,MAAM,CAACZ,KAAKC,MAAM,IAAIY,OAAOX,OAAO,CAACH,OAAQ;QAChD,IAAIK,MAAMC,OAAO,CAACJ,QAAQ;YACxB,KAAK,MAAMa,QAAQb,MAAO;gBACxBH,aAAaiB,MAAM,CAACf,KAAKO,uBAAuBO;YAClD;QACF,OAAO;YACLhB,aAAakB,GAAG,CAAChB,KAAKO,uBAAuBN;QAC/C;IACF;IACA,OAAOH;AACT;AAEA,OAAO,SAASmB,OACdC,MAAuB,EACvB,GAAGC,gBAAmC;IAEtC,KAAK,MAAMrB,gBAAgBqB,iBAAkB;QAC3C,KAAK,MAAMnB,OAAOF,aAAasB,IAAI,GAAI;YACrCF,OAAOG,MAAM,CAACrB;QAChB;QAEA,KAAK,MAAM,CAACA,KAAKC,MAAM,IAAIH,aAAaI,OAAO,GAAI;YACjDgB,OAAOH,MAAM,CAACf,KAAKC;QACrB;IACF;IAEA,OAAOiB;AACT","ignoreList":[0]}
+22
View File
@@ -0,0 +1,22 @@
/**
* The result of parsing a URL relative to a base URL.
*/ export function parseRelativeURL(url, base) {
const baseURL = typeof base === 'string' ? new URL(base) : base;
const relative = new URL(url, base);
// The URL is relative if the origin is the same as the base URL.
const isRelative = relative.origin === baseURL.origin;
return {
url: isRelative ? relative.toString().slice(baseURL.origin.length) : relative.toString(),
isRelative
};
}
/**
* Given a URL as a string and a base URL it will make the URL relative
* if the parsed protocol and host is the same as the one in the base
* URL. Otherwise it returns the same URL string.
*/ export function getRelativeURL(url, base) {
const relative = parseRelativeURL(url, base);
return relative.url;
}
//# sourceMappingURL=relativize-url.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/relativize-url.ts"],"sourcesContent":["/**\n * The result of parsing a URL relative to a base URL.\n */\nexport type RelativeURL = {\n /**\n * The relative URL. Either a URL including the origin or a relative URL.\n */\n url: string\n\n /**\n * Whether the URL is relative to the base URL.\n */\n isRelative: boolean\n}\n\nexport function parseRelativeURL(\n url: string | URL,\n base: string | URL\n): RelativeURL {\n const baseURL = typeof base === 'string' ? new URL(base) : base\n const relative = new URL(url, base)\n\n // The URL is relative if the origin is the same as the base URL.\n const isRelative = relative.origin === baseURL.origin\n\n return {\n url: isRelative\n ? relative.toString().slice(baseURL.origin.length)\n : relative.toString(),\n isRelative,\n }\n}\n\n/**\n * Given a URL as a string and a base URL it will make the URL relative\n * if the parsed protocol and host is the same as the one in the base\n * URL. Otherwise it returns the same URL string.\n */\nexport function getRelativeURL(url: string | URL, base: string | URL): string {\n const relative = parseRelativeURL(url, base)\n return relative.url\n}\n"],"names":["parseRelativeURL","url","base","baseURL","URL","relative","isRelative","origin","toString","slice","length","getRelativeURL"],"mappings":"AAAA;;CAEC,GAaD,OAAO,SAASA,iBACdC,GAAiB,EACjBC,IAAkB;IAElB,MAAMC,UAAU,OAAOD,SAAS,WAAW,IAAIE,IAAIF,QAAQA;IAC3D,MAAMG,WAAW,IAAID,IAAIH,KAAKC;IAE9B,iEAAiE;IACjE,MAAMI,aAAaD,SAASE,MAAM,KAAKJ,QAAQI,MAAM;IAErD,OAAO;QACLN,KAAKK,aACDD,SAASG,QAAQ,GAAGC,KAAK,CAACN,QAAQI,MAAM,CAACG,MAAM,IAC/CL,SAASG,QAAQ;QACrBF;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASK,eAAeV,GAAiB,EAAEC,IAAkB;IAClE,MAAMG,WAAWL,iBAAiBC,KAAKC;IACvC,OAAOG,SAASJ,GAAG;AACrB","ignoreList":[0]}
@@ -0,0 +1,36 @@
import { pathHasPrefix } from './path-has-prefix';
/**
* Given a path and a prefix it will remove the prefix when it exists in the
* given path. It ensures it matches exactly without containing extra chars
* and if the prefix is not there it will be noop.
*
* @param path The path to remove the prefix from.
* @param prefix The prefix to be removed.
*/ export function removePathPrefix(path, prefix) {
// If the path doesn't start with the prefix we can return it as is. This
// protects us from situations where the prefix is a substring of the path
// prefix such as:
//
// For prefix: /blog
//
// /blog -> true
// /blog/ -> true
// /blog/1 -> true
// /blogging -> false
// /blogging/ -> false
// /blogging/1 -> false
if (!pathHasPrefix(path, prefix)) {
return path;
}
// Remove the prefix from the path via slicing.
const withoutPrefix = path.slice(prefix.length);
// If the path without the prefix starts with a `/` we can return it as is.
if (withoutPrefix.startsWith('/')) {
return withoutPrefix;
}
// If the path without the prefix doesn't start with a `/` we need to add it
// back to the path to make sure it's a valid path.
return `/${withoutPrefix}`;
}
//# sourceMappingURL=remove-path-prefix.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/remove-path-prefix.ts"],"sourcesContent":["import { pathHasPrefix } from './path-has-prefix'\n\n/**\n * Given a path and a prefix it will remove the prefix when it exists in the\n * given path. It ensures it matches exactly without containing extra chars\n * and if the prefix is not there it will be noop.\n *\n * @param path The path to remove the prefix from.\n * @param prefix The prefix to be removed.\n */\nexport function removePathPrefix(path: string, prefix: string): string {\n // If the path doesn't start with the prefix we can return it as is. This\n // protects us from situations where the prefix is a substring of the path\n // prefix such as:\n //\n // For prefix: /blog\n //\n // /blog -> true\n // /blog/ -> true\n // /blog/1 -> true\n // /blogging -> false\n // /blogging/ -> false\n // /blogging/1 -> false\n if (!pathHasPrefix(path, prefix)) {\n return path\n }\n\n // Remove the prefix from the path via slicing.\n const withoutPrefix = path.slice(prefix.length)\n\n // If the path without the prefix starts with a `/` we can return it as is.\n if (withoutPrefix.startsWith('/')) {\n return withoutPrefix\n }\n\n // If the path without the prefix doesn't start with a `/` we need to add it\n // back to the path to make sure it's a valid path.\n return `/${withoutPrefix}`\n}\n"],"names":["pathHasPrefix","removePathPrefix","path","prefix","withoutPrefix","slice","length","startsWith"],"mappings":"AAAA,SAASA,aAAa,QAAQ,oBAAmB;AAEjD;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,IAAY,EAAEC,MAAc;IAC3D,yEAAyE;IACzE,0EAA0E;IAC1E,kBAAkB;IAClB,EAAE;IACF,oBAAoB;IACpB,EAAE;IACF,kBAAkB;IAClB,mBAAmB;IACnB,oBAAoB;IACpB,uBAAuB;IACvB,wBAAwB;IACxB,yBAAyB;IACzB,IAAI,CAACH,cAAcE,MAAMC,SAAS;QAChC,OAAOD;IACT;IAEA,+CAA+C;IAC/C,MAAME,gBAAgBF,KAAKG,KAAK,CAACF,OAAOG,MAAM;IAE9C,2EAA2E;IAC3E,IAAIF,cAAcG,UAAU,CAAC,MAAM;QACjC,OAAOH;IACT;IAEA,4EAA4E;IAC5E,mDAAmD;IACnD,OAAO,CAAC,CAAC,EAAEA,eAAe;AAC5B","ignoreList":[0]}
@@ -0,0 +1,11 @@
/**
* Removes the trailing slash for a given route or page path. Preserves the
* root page. Examples:
* - `/foo/bar/` -> `/foo/bar`
* - `/foo/bar` -> `/foo/bar`
* - `/` -> `/`
*/ export function removeTrailingSlash(route) {
return route.replace(/\/$/, '') || '/';
}
//# sourceMappingURL=remove-trailing-slash.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/remove-trailing-slash.ts"],"sourcesContent":["/**\n * Removes the trailing slash for a given route or page path. Preserves the\n * root page. Examples:\n * - `/foo/bar/` -> `/foo/bar`\n * - `/foo/bar` -> `/foo/bar`\n * - `/` -> `/`\n */\nexport function removeTrailingSlash(route: string) {\n return route.replace(/\\/$/, '') || '/'\n}\n"],"names":["removeTrailingSlash","route","replace"],"mappings":"AAAA;;;;;;CAMC,GACD,OAAO,SAASA,oBAAoBC,KAAa;IAC/C,OAAOA,MAAMC,OAAO,CAAC,OAAO,OAAO;AACrC","ignoreList":[0]}
@@ -0,0 +1,118 @@
import { InvariantError } from '../../invariant-error';
import { interceptionPrefixFromParamType } from './interception-prefix-from-param-type';
/**
* Extracts the param value from a path segment, handling interception markers
* based on the expected param type.
*
* @param pathSegment - The path segment to extract the value from
* @param params - The current params object for resolving dynamic param references
* @param paramType - The expected param type which may include interception marker info
* @returns The extracted param value
*/ function getParamValueFromSegment(pathSegment, params, paramType) {
// If the segment is dynamic, resolve it from the params object
if (pathSegment.type === 'dynamic') {
return params[pathSegment.param.paramName];
}
// If the paramType indicates this is an intercepted param, strip the marker
// that matches the interception marker in the param type
const interceptionPrefix = interceptionPrefixFromParamType(paramType);
if (interceptionPrefix === pathSegment.interceptionMarker) {
return pathSegment.name.replace(pathSegment.interceptionMarker, '');
}
// For static segments, use the name
return pathSegment.name;
}
/**
* Resolves a route parameter value from the route segments at the given depth.
* This shared logic is used by both extractPathnameRouteParamSegmentsFromLoaderTree
* and resolveRouteParamsFromTree.
*
* @param paramName - The parameter name to resolve
* @param paramType - The parameter type (dynamic, catchall, etc.)
* @param depth - The current depth in the route tree
* @param route - The normalized route containing segments
* @param params - The current params object (used to resolve embedded param references)
* @param options - Configuration options
* @returns The resolved parameter value, or undefined if it cannot be resolved
*/ export function resolveParamValue(paramName, paramType, depth, route, params) {
switch(paramType){
case 'catchall':
case 'optional-catchall':
case 'catchall-intercepted-(..)(..)':
case 'catchall-intercepted-(.)':
case 'catchall-intercepted-(..)':
case 'catchall-intercepted-(...)':
// For catchall routes, derive from pathname using depth to determine
// which segments to use
const processedSegments = [];
// Process segments to handle any embedded dynamic params
for(let index = depth; index < route.segments.length; index++){
const pathSegment = route.segments[index];
if (pathSegment.type === 'static') {
let value = pathSegment.name;
// For intercepted catch-all params, strip the marker from the first segment
const interceptionPrefix = interceptionPrefixFromParamType(paramType);
if (interceptionPrefix && index === depth && interceptionPrefix === pathSegment.interceptionMarker) {
// Strip the interception marker from the value
value = value.replace(pathSegment.interceptionMarker, '');
}
processedSegments.push(value);
} else {
// If the segment is a param placeholder, check if we have its value
if (!params.hasOwnProperty(pathSegment.param.paramName)) {
// If the segment is an optional catchall, we can break out of the
// loop because it's optional!
if (pathSegment.param.paramType === 'optional-catchall') {
break;
}
// Unknown param placeholder in pathname - can't derive full value
return undefined;
}
// If the segment matches a param, use the param value
// We don't encode values here as that's handled during retrieval.
const paramValue = params[pathSegment.param.paramName];
if (Array.isArray(paramValue)) {
processedSegments.push(...paramValue);
} else {
processedSegments.push(paramValue);
}
}
}
if (processedSegments.length > 0) {
return processedSegments;
} else if (paramType === 'optional-catchall') {
return undefined;
} else {
// We shouldn't be able to match a catchall segment without any path
// segments if it's not an optional catchall
throw Object.defineProperty(new InvariantError(`Unexpected empty path segments match for a route "${route.pathname}" with param "${paramName}" of type "${paramType}"`), "__NEXT_ERROR_CODE", {
value: "E931",
enumerable: false,
configurable: true
});
}
case 'dynamic':
case 'dynamic-intercepted-(..)(..)':
case 'dynamic-intercepted-(.)':
case 'dynamic-intercepted-(..)':
case 'dynamic-intercepted-(...)':
// For regular dynamic parameters, take the segment at this depth
if (depth < route.segments.length) {
const pathSegment = route.segments[depth];
// Check if the segment at this depth is a placeholder for an unknown param
if (pathSegment.type === 'dynamic' && !params.hasOwnProperty(pathSegment.param.paramName)) {
// The segment is a placeholder like [category] and we don't have the value
return undefined;
}
// If the segment matches a param, use the param value from params object
// Otherwise it's a static segment, just use it directly
// We don't encode values here as that's handled during retrieval
return getParamValueFromSegment(pathSegment, params, paramType);
}
return undefined;
default:
paramType;
}
}
//# sourceMappingURL=resolve-param-value.js.map
File diff suppressed because one or more lines are too long
+108
View File
@@ -0,0 +1,108 @@
import { getPathMatch } from './path-match';
import { matchHas, prepareDestination } from './prepare-destination';
import { removeTrailingSlash } from './remove-trailing-slash';
import { normalizeLocalePath } from '../../i18n/normalize-locale-path';
import { removeBasePath } from '../../../../client/remove-base-path';
import { parseRelativeUrl } from './parse-relative-url';
export default function resolveRewrites(asPath, pages, rewrites, query, resolveHref, locales) {
let matchedPage = false;
let externalDest = false;
let parsedAs = parseRelativeUrl(asPath);
let fsPathname = removeTrailingSlash(normalizeLocalePath(removeBasePath(parsedAs.pathname), locales).pathname);
let resolvedHref;
const handleRewrite = (rewrite)=>{
const matcher = getPathMatch(rewrite.source + (process.env.__NEXT_TRAILING_SLASH ? '(/)?' : ''), {
removeUnnamedParams: true,
strict: true
});
let params = matcher(parsedAs.pathname);
if ((rewrite.has || rewrite.missing) && params) {
const hasParams = matchHas({
headers: {
host: document.location.hostname,
'user-agent': navigator.userAgent
},
cookies: document.cookie.split('; ').reduce((acc, item)=>{
const [key, ...value] = item.split('=');
acc[key] = value.join('=');
return acc;
}, {})
}, parsedAs.query, rewrite.has, rewrite.missing);
if (hasParams) {
Object.assign(params, hasParams);
} else {
params = false;
}
}
if (params) {
if (!rewrite.destination) {
// this is a proxied rewrite which isn't handled on the client
externalDest = true;
return true;
}
const destRes = prepareDestination({
appendParamsToQuery: true,
destination: rewrite.destination,
params: params,
query: query
});
parsedAs = destRes.parsedDestination;
asPath = destRes.newUrl;
Object.assign(query, destRes.parsedDestination.query);
fsPathname = removeTrailingSlash(normalizeLocalePath(removeBasePath(asPath), locales).pathname);
if (pages.includes(fsPathname)) {
// check if we now match a page as this means we are done
// resolving the rewrites
matchedPage = true;
resolvedHref = fsPathname;
return true;
}
// check if we match a dynamic-route, if so we break the rewrites chain
resolvedHref = resolveHref(fsPathname);
if (resolvedHref !== asPath && pages.includes(resolvedHref)) {
matchedPage = true;
return true;
}
}
};
let finished = false;
for(let i = 0; i < rewrites.beforeFiles.length; i++){
// we don't end after match in beforeFiles to allow
// continuing through all beforeFiles rewrites
handleRewrite(rewrites.beforeFiles[i]);
}
matchedPage = pages.includes(fsPathname);
if (!matchedPage) {
if (!finished) {
for(let i = 0; i < rewrites.afterFiles.length; i++){
if (handleRewrite(rewrites.afterFiles[i])) {
finished = true;
break;
}
}
}
// check dynamic route before processing fallback rewrites
if (!finished) {
resolvedHref = resolveHref(fsPathname);
matchedPage = pages.includes(resolvedHref);
finished = matchedPage;
}
if (!finished) {
for(let i = 0; i < rewrites.fallback.length; i++){
if (handleRewrite(rewrites.fallback[i])) {
finished = true;
break;
}
}
}
}
return {
asPath,
parsedAs,
matchedPage,
resolvedHref,
externalDest
};
}
//# sourceMappingURL=resolve-rewrites.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
/**
* Client-safe utilities for route matching that don't import server-side
* utilities to avoid bundling issues with Turbopack
*/ import { pathToRegexp, compile, regexpToFunction } from 'next/dist/compiled/path-to-regexp';
import { hasAdjacentParameterIssues, normalizeAdjacentParameters, stripParameterSeparators, stripNormalizedSeparators } from '../../../../lib/route-pattern-normalizer';
/**
* Client-safe wrapper around pathToRegexp that handles path-to-regexp 6.3.0+ validation errors.
* This includes both "Can not repeat without prefix/suffix" and "Must have text between parameters" errors.
*/ export function safePathToRegexp(route, keys, options) {
if (typeof route !== 'string') {
return pathToRegexp(route, keys, options);
}
// Check if normalization is needed and cache the result
const needsNormalization = hasAdjacentParameterIssues(route);
const routeToUse = needsNormalization ? normalizeAdjacentParameters(route) : route;
try {
return pathToRegexp(routeToUse, keys, options);
} catch (error) {
// Only try normalization if we haven't already normalized
if (!needsNormalization) {
try {
const normalizedRoute = normalizeAdjacentParameters(route);
return pathToRegexp(normalizedRoute, keys, options);
} catch (retryError) {
// If that doesn't work, fall back to original error
throw error;
}
}
throw error;
}
}
/**
* Client-safe wrapper around compile that handles path-to-regexp 6.3.0+ validation errors.
* No server-side error reporting to avoid bundling issues.
* When normalization is applied, the returned compiler function automatically strips
* the internal separator from the output URL.
*/ export function safeCompile(route, options) {
// Check if normalization is needed and cache the result
const needsNormalization = hasAdjacentParameterIssues(route);
const routeToUse = needsNormalization ? normalizeAdjacentParameters(route) : route;
try {
const compiler = compile(routeToUse, options);
// If we normalized the route, wrap the compiler to strip separators from output
// The normalization inserts _NEXTSEP_ as a literal string in the pattern to satisfy
// path-to-regexp validation, but we don't want it in the final compiled URL
if (needsNormalization) {
return (params)=>{
return stripNormalizedSeparators(compiler(params));
};
}
return compiler;
} catch (error) {
// Only try normalization if we haven't already normalized
if (!needsNormalization) {
try {
const normalizedRoute = normalizeAdjacentParameters(route);
const compiler = compile(normalizedRoute, options);
// Wrap the compiler to strip separators from output
return (params)=>{
return stripNormalizedSeparators(compiler(params));
};
} catch (retryError) {
// If that doesn't work, fall back to original error
throw error;
}
}
throw error;
}
}
/**
* Client-safe wrapper around regexpToFunction that automatically cleans parameters.
*/ export function safeRegexpToFunction(regexp, keys) {
const originalMatcher = regexpToFunction(regexp, keys || []);
return (pathname)=>{
const result = originalMatcher(pathname);
if (!result) return false;
// Clean parameters before returning
return {
...result,
params: stripParameterSeparators(result.params)
};
};
}
/**
* Safe wrapper for route matcher functions that automatically cleans parameters.
* This is client-safe and doesn't import path-to-regexp.
*/ export function safeRouteMatcher(matcherFn) {
return (pathname)=>{
const result = matcherFn(pathname);
if (!result) return false;
// Clean parameters before returning
return stripParameterSeparators(result);
};
}
//# sourceMappingURL=route-match-utils.js.map
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
import { DecodeError } from '../../utils';
import { safeRouteMatcher } from './route-match-utils';
export function getRouteMatcher({ re, groups }) {
const rawMatcher = (pathname)=>{
const routeMatch = re.exec(pathname);
if (!routeMatch) return false;
const decode = (param)=>{
try {
return decodeURIComponent(param);
} catch {
throw Object.defineProperty(new DecodeError('failed to decode param'), "__NEXT_ERROR_CODE", {
value: "E528",
enumerable: false,
configurable: true
});
}
};
const params = {};
for (const [key, group] of Object.entries(groups)){
const match = routeMatch[group.pos];
if (match !== undefined) {
if (group.repeat) {
params[key] = match.split('/').map((entry)=>decode(entry));
} else {
params[key] = decode(match);
}
}
}
return params;
};
// Wrap with safe matcher to handle parameter cleaning
return safeRouteMatcher(rawMatcher);
}
//# sourceMappingURL=route-matcher.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/shared/lib/router/utils/route-matcher.ts"],"sourcesContent":["import type { Group } from './route-regex'\nimport { DecodeError } from '../../utils'\nimport type { Params } from '../../../../server/request/params'\nimport { safeRouteMatcher } from './route-match-utils'\n\nexport interface RouteMatchFn {\n (pathname: string): false | Params\n}\n\ntype RouteMatcherOptions = {\n // We only use the exec method of the RegExp object. This helps us avoid using\n // type assertions that the passed in properties are of the correct type.\n re: Pick<RegExp, 'exec'>\n groups: Record<string, Group>\n}\n\nexport function getRouteMatcher({\n re,\n groups,\n}: RouteMatcherOptions): RouteMatchFn {\n const rawMatcher = (pathname: string) => {\n const routeMatch = re.exec(pathname)\n if (!routeMatch) return false\n\n const decode = (param: string) => {\n try {\n return decodeURIComponent(param)\n } catch {\n throw new DecodeError('failed to decode param')\n }\n }\n\n const params: Params = {}\n for (const [key, group] of Object.entries(groups)) {\n const match = routeMatch[group.pos]\n if (match !== undefined) {\n if (group.repeat) {\n params[key] = match.split('/').map((entry) => decode(entry))\n } else {\n params[key] = decode(match)\n }\n }\n }\n\n return params\n }\n\n // Wrap with safe matcher to handle parameter cleaning\n return safeRouteMatcher(rawMatcher)\n}\n"],"names":["DecodeError","safeRouteMatcher","getRouteMatcher","re","groups","rawMatcher","pathname","routeMatch","exec","decode","param","decodeURIComponent","params","key","group","Object","entries","match","pos","undefined","repeat","split","map","entry"],"mappings":"AACA,SAASA,WAAW,QAAQ,cAAa;AAEzC,SAASC,gBAAgB,QAAQ,sBAAqB;AAatD,OAAO,SAASC,gBAAgB,EAC9BC,EAAE,EACFC,MAAM,EACc;IACpB,MAAMC,aAAa,CAACC;QAClB,MAAMC,aAAaJ,GAAGK,IAAI,CAACF;QAC3B,IAAI,CAACC,YAAY,OAAO;QAExB,MAAME,SAAS,CAACC;YACd,IAAI;gBACF,OAAOC,mBAAmBD;YAC5B,EAAE,OAAM;gBACN,MAAM,qBAAyC,CAAzC,IAAIV,YAAY,2BAAhB,qBAAA;2BAAA;gCAAA;kCAAA;gBAAwC;YAChD;QACF;QAEA,MAAMY,SAAiB,CAAC;QACxB,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACZ,QAAS;YACjD,MAAMa,QAAQV,UAAU,CAACO,MAAMI,GAAG,CAAC;YACnC,IAAID,UAAUE,WAAW;gBACvB,IAAIL,MAAMM,MAAM,EAAE;oBAChBR,MAAM,CAACC,IAAI,GAAGI,MAAMI,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,QAAUd,OAAOc;gBACvD,OAAO;oBACLX,MAAM,CAACC,IAAI,GAAGJ,OAAOQ;gBACvB;YACF;QACF;QAEA,OAAOL;IACT;IAEA,sDAAsD;IACtD,OAAOX,iBAAiBI;AAC1B","ignoreList":[0]}
+244
View File
@@ -0,0 +1,244 @@
import { NEXT_INTERCEPTION_MARKER_PREFIX, NEXT_QUERY_PARAM_PREFIX } from '../../../../lib/constants';
import { INTERCEPTION_ROUTE_MARKERS } from './interception-routes';
import { escapeStringRegexp } from '../../escape-regexp';
import { removeTrailingSlash } from './remove-trailing-slash';
import { PARAMETER_PATTERN, parseMatchedParameter } from './get-dynamic-param';
function getParametrizedRoute(route, includeSuffix, includePrefix) {
const groups = {};
let groupIndex = 1;
const segments = [];
for (const segment of removeTrailingSlash(route).slice(1).split('/')){
const markerMatch = INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m));
const paramMatches = segment.match(PARAMETER_PATTERN) // Check for parameters
;
if (markerMatch && paramMatches && paramMatches[2]) {
const { key, optional, repeat } = parseMatchedParameter(paramMatches[2]);
groups[key] = {
pos: groupIndex++,
repeat,
optional
};
segments.push(`/${escapeStringRegexp(markerMatch)}([^/]+?)`);
} else if (paramMatches && paramMatches[2]) {
const { key, repeat, optional } = parseMatchedParameter(paramMatches[2]);
groups[key] = {
pos: groupIndex++,
repeat,
optional
};
if (includePrefix && paramMatches[1]) {
segments.push(`/${escapeStringRegexp(paramMatches[1])}`);
}
let s = repeat ? optional ? '(?:/(.+?))?' : '/(.+?)' : '/([^/]+?)';
// Remove the leading slash if includePrefix already added it.
if (includePrefix && paramMatches[1]) {
s = s.substring(1);
}
segments.push(s);
} else {
segments.push(`/${escapeStringRegexp(segment)}`);
}
// If there's a suffix, add it to the segments if it's enabled.
if (includeSuffix && paramMatches && paramMatches[3]) {
segments.push(escapeStringRegexp(paramMatches[3]));
}
}
return {
parameterizedRoute: segments.join(''),
groups
};
}
/**
* From a normalized route this function generates a regular expression and
* a corresponding groups object intended to be used to store matching groups
* from the regular expression.
*/ export function getRouteRegex(normalizedRoute, { includeSuffix = false, includePrefix = false, excludeOptionalTrailingSlash = false } = {}) {
const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute, includeSuffix, includePrefix);
let re = parameterizedRoute;
if (!excludeOptionalTrailingSlash) {
re += '(?:/)?';
}
return {
re: new RegExp(`^${re}$`),
groups: groups
};
}
/**
* Builds a function to generate a minimal routeKey using only a-z and minimal
* number of characters.
*/ function buildGetSafeRouteKey() {
let i = 0;
return ()=>{
let routeKey = '';
let j = ++i;
while(j > 0){
routeKey += String.fromCharCode(97 + (j - 1) % 26);
j = Math.floor((j - 1) / 26);
}
return routeKey;
};
}
function getSafeKeyFromSegment({ interceptionMarker, getSafeRouteKey, segment, routeKeys, keyPrefix, backreferenceDuplicateKeys }) {
const { key, optional, repeat } = parseMatchedParameter(segment);
// replace any non-word characters since they can break
// the named regex
let cleanedKey = key.replace(/\W/g, '');
if (keyPrefix) {
cleanedKey = `${keyPrefix}${cleanedKey}`;
}
let invalidKey = false;
// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true;
}
if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) {
invalidKey = true;
}
if (invalidKey) {
cleanedKey = getSafeRouteKey();
}
const duplicateKey = cleanedKey in routeKeys;
if (keyPrefix) {
routeKeys[cleanedKey] = `${keyPrefix}${key}`;
} else {
routeKeys[cleanedKey] = key;
}
// if the segment has an interception marker, make sure that's part of the regex pattern
// this is to ensure that the route with the interception marker doesn't incorrectly match
// the non-intercepted route (ie /app/(.)[username] should not match /app/[username])
const interceptionPrefix = interceptionMarker ? escapeStringRegexp(interceptionMarker) : '';
let pattern;
if (duplicateKey && backreferenceDuplicateKeys) {
// Use a backreference to the key to ensure that the key is the same value
// in each of the placeholders.
pattern = `\\k<${cleanedKey}>`;
} else if (repeat) {
pattern = `(?<${cleanedKey}>.+?)`;
} else {
pattern = `(?<${cleanedKey}>[^/]+?)`;
}
return {
key,
pattern: optional ? `(?:/${interceptionPrefix}${pattern})?` : `/${interceptionPrefix}${pattern}`,
cleanedKey: cleanedKey,
optional,
repeat
};
}
function getNamedParametrizedRoute(route, prefixRouteKeys, includeSuffix, includePrefix, backreferenceDuplicateKeys, reference = {
names: {},
intercepted: {}
}) {
const getSafeRouteKey = buildGetSafeRouteKey();
const routeKeys = {};
const segments = [];
const inverseParts = [];
// Ensure we don't mutate the original reference object.
reference = structuredClone(reference);
for (const segment of removeTrailingSlash(route).slice(1).split('/')){
const hasInterceptionMarker = INTERCEPTION_ROUTE_MARKERS.some((m)=>segment.startsWith(m));
const paramMatches = segment.match(PARAMETER_PATTERN) // Check for parameters
;
const interceptionMarker = hasInterceptionMarker ? paramMatches?.[1] : undefined;
let keyPrefix;
if (interceptionMarker && paramMatches?.[2]) {
keyPrefix = prefixRouteKeys ? NEXT_INTERCEPTION_MARKER_PREFIX : undefined;
reference.intercepted[paramMatches[2]] = interceptionMarker;
} else if (paramMatches?.[2] && reference.intercepted[paramMatches[2]]) {
keyPrefix = prefixRouteKeys ? NEXT_INTERCEPTION_MARKER_PREFIX : undefined;
} else {
keyPrefix = prefixRouteKeys ? NEXT_QUERY_PARAM_PREFIX : undefined;
}
if (interceptionMarker && paramMatches && paramMatches[2]) {
// If there's an interception marker, add it to the segments.
const { key, pattern, cleanedKey, repeat, optional } = getSafeKeyFromSegment({
getSafeRouteKey,
interceptionMarker,
segment: paramMatches[2],
routeKeys,
keyPrefix,
backreferenceDuplicateKeys
});
segments.push(pattern);
inverseParts.push(`/${paramMatches[1]}:${reference.names[key] ?? cleanedKey}${repeat ? optional ? '*' : '+' : ''}`);
reference.names[key] ??= cleanedKey;
} else if (paramMatches && paramMatches[2]) {
// If there's a prefix, add it to the segments if it's enabled.
if (includePrefix && paramMatches[1]) {
segments.push(`/${escapeStringRegexp(paramMatches[1])}`);
inverseParts.push(`/${paramMatches[1]}`);
}
const { key, pattern, cleanedKey, repeat, optional } = getSafeKeyFromSegment({
getSafeRouteKey,
segment: paramMatches[2],
routeKeys,
keyPrefix,
backreferenceDuplicateKeys
});
// Remove the leading slash if includePrefix already added it.
let s = pattern;
if (includePrefix && paramMatches[1]) {
s = s.substring(1);
}
segments.push(s);
inverseParts.push(`/:${reference.names[key] ?? cleanedKey}${repeat ? optional ? '*' : '+' : ''}`);
reference.names[key] ??= cleanedKey;
} else {
segments.push(`/${escapeStringRegexp(segment)}`);
inverseParts.push(`/${segment}`);
}
// If there's a suffix, add it to the segments if it's enabled.
if (includeSuffix && paramMatches && paramMatches[3]) {
segments.push(escapeStringRegexp(paramMatches[3]));
inverseParts.push(paramMatches[3]);
}
}
return {
namedParameterizedRoute: segments.join(''),
routeKeys,
pathToRegexpPattern: inverseParts.join(''),
reference
};
}
/**
* This function extends `getRouteRegex` generating also a named regexp where
* each group is named along with a routeKeys object that indexes the assigned
* named group with its corresponding key. When the routeKeys need to be
* prefixed to uniquely identify internally the "prefixRouteKey" arg should
* be "true" currently this is only the case when creating the routes-manifest
* during the build
*/ export function getNamedRouteRegex(normalizedRoute, options) {
const result = getNamedParametrizedRoute(normalizedRoute, options.prefixRouteKeys, options.includeSuffix ?? false, options.includePrefix ?? false, options.backreferenceDuplicateKeys ?? false, options.reference);
let namedRegex = result.namedParameterizedRoute;
if (!options.excludeOptionalTrailingSlash) {
namedRegex += '(?:/)?';
}
return {
...getRouteRegex(normalizedRoute, options),
namedRegex: `^${namedRegex}$`,
routeKeys: result.routeKeys,
pathToRegexpPattern: result.pathToRegexpPattern,
reference: result.reference
};
}
/**
* Generates a named regexp.
* This is intended to be using for build time only.
*/ export function getNamedMiddlewareRegex(normalizedRoute, options) {
const { parameterizedRoute } = getParametrizedRoute(normalizedRoute, false, false);
const { catchAll = true } = options;
if (parameterizedRoute === '/') {
let catchAllRegex = catchAll ? '.*' : '';
return {
namedRegex: `^/${catchAllRegex}$`
};
}
const { namedParameterizedRoute } = getNamedParametrizedRoute(normalizedRoute, false, false, false, false, undefined);
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : '';
return {
namedRegex: `^${namedParameterizedRoute}${catchAllGroupedRegex}$`
};
}
//# sourceMappingURL=route-regex.js.map
File diff suppressed because one or more lines are too long
+213
View File
@@ -0,0 +1,213 @@
/**
* A route that can be sorted by specificity.
*/ /**
* Determines the specificity of a route segment for sorting purposes.
*
* In Next.js routing, more specific routes should match before less specific ones.
* This function returns a numeric value where lower numbers indicate higher specificity.
*
* Specificity order (most to least specific):
* 1. Static segments (e.g., "about", "api") - return 0
* 2. Dynamic segments (e.g., "[id]", "[slug]") - return 1
* 3. Catch-all segments (e.g., "[...slug]") - return 2
* 4. Optional catch-all segments (e.g., "[[...slug]]") - return 3
*
* @param segment - A single path segment (e.g., "api", "[id]", "[...slug]")
* @returns A numeric specificity value (0-3, where 0 is most specific)
*/ export function getSegmentSpecificity(segment) {
// Static segments are most specific - they match exactly one path
if (!segment.includes('[')) {
return 0;
}
// Optional catch-all [[...param]] is least specific - matches zero or more segments
if (segment.startsWith('[[...') && segment.endsWith(']]')) {
return 3;
}
// Catch-all [...param] is less specific - matches one or more segments
if (segment.startsWith('[...') && segment.endsWith(']')) {
return 2;
}
// Regular dynamic [param] is more specific than catch-all - matches exactly one segment
if (segment.startsWith('[') && segment.endsWith(']')) {
return 1;
}
// Default to static (fallback case)
return 0;
}
/**
* Compares two route paths using a depth-first traversal approach.
*
* This function implements a deterministic comparison that sorts routes by specificity:
* 1. More specific routes come first (fewer dynamic segments)
* 2. Shorter routes are more specific than longer ones
* 3. Routes with same specificity are sorted lexicographically
*
* The comparison is done segment by segment, left to right, similar to how
* you would traverse a route tree in depth-first order.
*
* @param pathA - First route path to compare (e.g., "/api/users/[id]")
* @param pathB - Second route path to compare (e.g., "/api/[...slug]")
* @returns Negative if pathA is more specific, positive if pathB is more specific, 0 if equal
*/ export function compareRouteSegments(pathA, pathB) {
// Split paths into segments, removing empty strings from leading/trailing slashes
const segmentsA = pathA.split('/').filter(Boolean);
const segmentsB = pathB.split('/').filter(Boolean);
// Compare segment by segment up to the length of the longer path
const maxLength = Math.max(segmentsA.length, segmentsB.length);
for(let i = 0; i < maxLength; i++){
const segA = segmentsA[i] || '';
const segB = segmentsB[i] || '';
// Handle length differences: shorter routes are MORE specific
// Example: "/api" is more specific than "/api/users"
if (!segA && segB) return -1 // pathA is shorter, so more specific
;
if (segA && !segB) return 1 // pathB is shorter, so more specific
;
if (!segA && !segB) return 0 // Both paths ended, they're equal
;
// Compare segment specificity using our specificity scoring
const specificityA = getSegmentSpecificity(segA);
const specificityB = getSegmentSpecificity(segB);
// Lower specificity number = more specific route
// Example: "api" (0) vs "[slug]" (1) - "api" wins
if (specificityA !== specificityB) {
return specificityA - specificityB;
}
// If segments have same specificity, compare lexicographically for determinism
// Example: "[id]" vs "[slug]" - "[id]" comes first alphabetically
if (segA !== segB) {
return segA.localeCompare(segB);
}
// Segments are identical, continue to next segment
}
// All segments compared equally
return 0;
}
/**
* Compares two complete routes for sorting purposes.
*
* Routes are compared with a two-tier priority system:
* 1. Primary: Compare by source path specificity
* 2. Secondary: If sources are equal, compare by page path specificity
*
* This ensures that routes are primarily organized by their source patterns,
* with page-specific variations grouped together.
*
* @param a - First route to compare
* @param b - Second route to compare
* @returns Negative if route a should come first, positive if route b should come first, 0 if equal
*/ function compareSortableRoutes(a, b) {
// First compare by source specificity - this is the primary sorting criterion
// Source represents the original route pattern and takes precedence
const sourceResult = compareRouteSegments(a.sourcePage, b.sourcePage);
if (sourceResult !== 0) return sourceResult;
// If sources are identical, compare by page specificity as a tiebreaker
// Page represents the final rendered route and provides secondary ordering
return compareRouteSegments(a.page, b.page);
}
/**
* Sorts an array of routes by specificity using a deterministic depth-first traversal approach.
*
* This function implements Next.js route matching priority where more specific routes
* should be matched before less specific ones. The sorting is deterministic and stable,
* meaning identical inputs will always produce identical outputs.
*
* Sorting criteria (in order of priority):
* 1. Source path specificity (primary)
* 2. Page path specificity (secondary)
* 3. Lexicographic ordering (tertiary, for determinism)
*
* Examples of specificity order:
* - "/api/users" (static) comes before "/api/[slug]" (dynamic)
* - "/api/[id]" (dynamic) comes before "/api/[...slug]" (catch-all)
* - "/api/[...slug]" (catch-all) comes before "/api/[[...slug]]" (optional catch-all)
*
* @param routes - Array of routes to sort
* @returns New sorted array (does not mutate input)
*/ export function sortSortableRoutes(routes) {
// Because sort is always in-place, we need to create a shallow copy to avoid
// mutating the input array.
return [
...routes
].sort(compareSortableRoutes);
}
/**
* Sorts an array of pages by specificity using a deterministic depth-first
* traversal approach.
*
* @param pages - Array of pages to sort
* @returns New sorted array (does not mutate input)
*/ export function sortPages(pages) {
// Because sort is always in-place, we need to create a shallow copy to avoid
// mutating the input array.
return [
...pages
].sort(compareRouteSegments);
}
/**
* Sorts an object by specificity of the keys using a deterministic depth-first traversal approach.
*
* @param obj - object of pages to sort
* @returns New sorted object (does not mutate input)
*/ export function sortPagesObject(obj) {
// Because sort is always in-place, we need to create a shallow copy to avoid
// mutating the input.
return Object.keys(obj).sort(compareRouteSegments).reduce((acc, key)=>{
acc[key] = obj[key];
return acc;
}, {});
}
/**
* Sorts an array of objects by sourcePage and page using a deterministic
* depth-first traversal approach.
*
* @param objects - Array of objects to sort
* @param getter - Function to get the sourcePage and page from an object
* @returns New sorted array (does not mutate input)
*/ export function sortSortableRouteObjects(objects, getter) {
// Create a SortableRoute for each object.
const routes = [];
for (const object of objects){
const route = getter(object);
routes.push({
...route,
object
});
}
// In-place sort the SortableRoutes.
routes.sort(compareSortableRoutes);
// Map the sorted SortableRoutes back to the original objects.
return routes.map(({ object })=>object);
}
/**
* Sorts an array of objects by page using a deterministic depth-first traversal
* approach.
*
* @param objects - Array of objects to sort
* @param getter - Function to get the page from an object
* @returns New sorted array (does not mutate input)
*/ export function sortPageObjects(objects, getter) {
const indexes = {};
const pages = new Set();
for(let i = 0; i < objects.length; i++){
const object = objects[i];
const page = getter(object);
indexes[page]?.push(i) || (indexes[page] = [
i
]);
pages.add(page);
}
// Sort the unique pages.
const sortedPages = Array.from(pages).sort(compareRouteSegments);
// Map the sorted pages back to the original objects.
return sortedPages.reduce((sortedObjects, page)=>{
// Add all objects for this page to the sorted array.
for (const i of indexes[page]){
sortedObjects.push(objects[i]);
}
// Return the sorted array.
return sortedObjects;
}, []);
}
//# sourceMappingURL=sortable-routes.js.map
File diff suppressed because one or more lines are too long
+226
View File
@@ -0,0 +1,226 @@
class UrlNode {
insert(urlPath) {
this._insert(urlPath.split('/').filter(Boolean), [], false);
}
smoosh() {
return this._smoosh();
}
_smoosh(prefix = '/') {
const childrenPaths = [
...this.children.keys()
].sort();
if (this.slugName !== null) {
childrenPaths.splice(childrenPaths.indexOf('[]'), 1);
}
if (this.restSlugName !== null) {
childrenPaths.splice(childrenPaths.indexOf('[...]'), 1);
}
if (this.optionalRestSlugName !== null) {
childrenPaths.splice(childrenPaths.indexOf('[[...]]'), 1);
}
const routes = childrenPaths.map((c)=>this.children.get(c)._smoosh(`${prefix}${c}/`)).reduce((prev, curr)=>[
...prev,
...curr
], []);
if (this.slugName !== null) {
routes.push(...this.children.get('[]')._smoosh(`${prefix}[${this.slugName}]/`));
}
if (!this.placeholder) {
const r = prefix === '/' ? '/' : prefix.slice(0, -1);
if (this.optionalRestSlugName != null) {
throw Object.defineProperty(new Error(`You cannot define a route with the same specificity as a optional catch-all route ("${r}" and "${r}[[...${this.optionalRestSlugName}]]").`), "__NEXT_ERROR_CODE", {
value: "E458",
enumerable: false,
configurable: true
});
}
routes.unshift(r);
}
if (this.restSlugName !== null) {
routes.push(...this.children.get('[...]')._smoosh(`${prefix}[...${this.restSlugName}]/`));
}
if (this.optionalRestSlugName !== null) {
routes.push(...this.children.get('[[...]]')._smoosh(`${prefix}[[...${this.optionalRestSlugName}]]/`));
}
return routes;
}
_insert(urlPaths, slugNames, isCatchAll) {
if (urlPaths.length === 0) {
this.placeholder = false;
return;
}
if (isCatchAll) {
throw Object.defineProperty(new Error(`Catch-all must be the last part of the URL.`), "__NEXT_ERROR_CODE", {
value: "E392",
enumerable: false,
configurable: true
});
}
// The next segment in the urlPaths list
let nextSegment = urlPaths[0];
// Check if the segment matches `[something]`
if (nextSegment.startsWith('[') && nextSegment.endsWith(']')) {
// Strip `[` and `]`, leaving only `something`
let segmentName = nextSegment.slice(1, -1);
let isOptional = false;
if (segmentName.startsWith('[') && segmentName.endsWith(']')) {
// Strip optional `[` and `]`, leaving only `something`
segmentName = segmentName.slice(1, -1);
isOptional = true;
}
if (segmentName.startsWith('…')) {
throw Object.defineProperty(new Error(`Detected a three-dot character ('…') at ('${segmentName}'). Did you mean ('...')?`), "__NEXT_ERROR_CODE", {
value: "E147",
enumerable: false,
configurable: true
});
}
if (segmentName.startsWith('...')) {
// Strip `...`, leaving only `something`
segmentName = segmentName.substring(3);
isCatchAll = true;
}
if (segmentName.startsWith('[') || segmentName.endsWith(']')) {
throw Object.defineProperty(new Error(`Segment names may not start or end with extra brackets ('${segmentName}').`), "__NEXT_ERROR_CODE", {
value: "E421",
enumerable: false,
configurable: true
});
}
if (segmentName.startsWith('.')) {
throw Object.defineProperty(new Error(`Segment names may not start with erroneous periods ('${segmentName}').`), "__NEXT_ERROR_CODE", {
value: "E288",
enumerable: false,
configurable: true
});
}
function handleSlug(previousSlug, nextSlug) {
if (previousSlug !== null) {
// If the specific segment already has a slug but the slug is not `something`
// This prevents collisions like:
// pages/[post]/index.js
// pages/[id]/index.js
// Because currently multiple dynamic params on the same segment level are not supported
if (previousSlug !== nextSlug) {
// TODO: This error seems to be confusing for users, needs an error link, the description can be based on above comment.
throw Object.defineProperty(new Error(`You cannot use different slug names for the same dynamic path ('${previousSlug}' !== '${nextSlug}').`), "__NEXT_ERROR_CODE", {
value: "E337",
enumerable: false,
configurable: true
});
}
}
slugNames.forEach((slug)=>{
if (slug === nextSlug) {
throw Object.defineProperty(new Error(`You cannot have the same slug name "${nextSlug}" repeat within a single dynamic path`), "__NEXT_ERROR_CODE", {
value: "E247",
enumerable: false,
configurable: true
});
}
if (slug.replace(/\W/g, '') === nextSegment.replace(/\W/g, '')) {
throw Object.defineProperty(new Error(`You cannot have the slug names "${slug}" and "${nextSlug}" differ only by non-word symbols within a single dynamic path`), "__NEXT_ERROR_CODE", {
value: "E499",
enumerable: false,
configurable: true
});
}
});
slugNames.push(nextSlug);
}
if (isCatchAll) {
if (isOptional) {
if (this.restSlugName != null) {
throw Object.defineProperty(new Error(`You cannot use both an required and optional catch-all route at the same level ("[...${this.restSlugName}]" and "${urlPaths[0]}" ).`), "__NEXT_ERROR_CODE", {
value: "E299",
enumerable: false,
configurable: true
});
}
handleSlug(this.optionalRestSlugName, segmentName);
// slugName is kept as it can only be one particular slugName
this.optionalRestSlugName = segmentName;
// nextSegment is overwritten to [[...]] so that it can later be sorted specifically
nextSegment = '[[...]]';
} else {
if (this.optionalRestSlugName != null) {
throw Object.defineProperty(new Error(`You cannot use both an optional and required catch-all route at the same level ("[[...${this.optionalRestSlugName}]]" and "${urlPaths[0]}").`), "__NEXT_ERROR_CODE", {
value: "E300",
enumerable: false,
configurable: true
});
}
handleSlug(this.restSlugName, segmentName);
// slugName is kept as it can only be one particular slugName
this.restSlugName = segmentName;
// nextSegment is overwritten to [...] so that it can later be sorted specifically
nextSegment = '[...]';
}
} else {
if (isOptional) {
throw Object.defineProperty(new Error(`Optional route parameters are not yet supported ("${urlPaths[0]}").`), "__NEXT_ERROR_CODE", {
value: "E435",
enumerable: false,
configurable: true
});
}
handleSlug(this.slugName, segmentName);
// slugName is kept as it can only be one particular slugName
this.slugName = segmentName;
// nextSegment is overwritten to [] so that it can later be sorted specifically
nextSegment = '[]';
}
}
// If this UrlNode doesn't have the nextSegment yet we create a new child UrlNode
if (!this.children.has(nextSegment)) {
this.children.set(nextSegment, new UrlNode());
}
this.children.get(nextSegment)._insert(urlPaths.slice(1), slugNames, isCatchAll);
}
constructor(){
this.placeholder = true;
this.children = new Map();
this.slugName = null;
this.restSlugName = null;
this.optionalRestSlugName = null;
}
}
/**
* @deprecated Use `sortSortableRoutes` or `sortPages` instead.
*/ export function getSortedRoutes(normalizedPages) {
// First the UrlNode is created, and every UrlNode can have only 1 dynamic segment
// Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js
// Only 1 dynamic segment per nesting level
// So in the case that is test/integration/dynamic-routing it'll be this:
// pages/[post]/comments.js
// pages/blog/[post]/comment/[id].js
// Both are fine because `pages/[post]` and `pages/blog` are on the same level
// So in this case `UrlNode` created here has `this.slugName === 'post'`
// And since your PR passed through `slugName` as an array basically it'd including it in too many possibilities
// Instead what has to be passed through is the upwards path's dynamic names
const root = new UrlNode();
// Here the `root` gets injected multiple paths, and insert will break them up into sublevels
normalizedPages.forEach((pagePath)=>root.insert(pagePath));
// Smoosh will then sort those sublevels up to the point where you get the correct route definition priority
return root.smoosh();
}
/**
* @deprecated Use `sortSortableRouteObjects` or `sortPageObjects` instead.
*/ export function getSortedRouteObjects(objects, getter) {
// We're assuming here that all the pathnames are unique, that way we can
// sort the list and use the index as the key.
const indexes = {};
const pathnames = [];
for(let i = 0; i < objects.length; i++){
const pathname = getter(objects[i]);
indexes[pathname] = i;
pathnames[i] = pathname;
}
// Sort the pathnames.
const sorted = getSortedRoutes(pathnames);
// Map the sorted pathnames back to the original objects using the new sorted
// index.
return sorted.map((pathname)=>objects[indexes[pathname]]);
}
//# sourceMappingURL=sorted-routes.js.map
File diff suppressed because one or more lines are too long