including-modules

This commit is contained in:
Kismet Hasanaj
2026-05-03 00:14:08 +02:00
parent ec83a0d879
commit 39a8a128be
20434 changed files with 3906546 additions and 3 deletions
+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]}
+50
View File
@@ -0,0 +1,50 @@
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}`;
}, ''));
}
/**
* 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 * 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","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;;;CAGC,GACD,OAAO,SAASI,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]}
+172
View File
@@ -0,0 +1,172 @@
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) {
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
]
};
}
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
});
}
return {
param: segmentKey,
// The value that is passed to user code.
value,
// The value that is rendered in the router tree.
treeSegment: [
segmentKey,
Array.isArray(value) ? value.join('/') : value,
dynamicParamType
],
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,19 @@
import { DEFAULT_SEGMENT_KEY } from '../../segment';
export function parseLoaderTree(tree) {
const [segment, parallelRoutes, modules] = 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
};
}
//# 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] = 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 }\n}\n"],"names":["DEFAULT_SEGMENT_KEY","parseLoaderTree","tree","segment","parallelRoutes","modules","layout","template","page","defaultPage","conventionPath"],"mappings":"AAAA,SAASA,mBAAmB,QAAQ,gBAAe;AAGnD,OAAO,SAASC,gBAAgBC,IAAgB;IAC9C,MAAM,CAACC,SAASC,gBAAgBC,QAAQ,GAAGH;IAC3C,MAAM,EAAEI,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,IAAI,EAAEG,IAAI,EAAE,GAAGH;IACf,sEAAsE;IACtE,2DAA2D;IAC3DG,OAAOL,YAAYH,sBAAsBK,QAAQI,WAAW,GAAGD;IAE/D,MAAME,iBAAiBJ,QAAQ,CAAC,EAAE,IAAIC,UAAU,CAAC,EAAE,IAAIC,MAAM,CAAC,EAAE;IAEhE,OAAO;QACLA;QACAL;QACAE;QACA,6CAA6C,GAC7CK;QACAN;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,26 @@
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 } = 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 {
pathname,
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: undefined
};
}
//# 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 hash: string\n href: string\n pathname: string\n query: ParsedUrlQuery\n search: string\n slashes: undefined\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 } = new URL(\n url,\n resolvedBase\n )\n\n if (origin !== globalBase.origin) {\n throw new Error(`invariant: invalid relative URL, router received ${url}`)\n }\n\n return {\n pathname,\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: undefined,\n }\n}\n"],"names":["getLocationOrigin","searchParamsToUrlQuery","parseRelativeUrl","url","base","parseQuery","globalBase","URL","window","resolvedBase","startsWith","location","href","pathname","searchParams","search","hash","origin","Error","query","undefined","slice","length","slashes"],"mappings":"AACA,SAASA,iBAAiB,QAAQ,cAAa;AAC/C,SAASC,sBAAsB,QAAQ,gBAAe;AA2BtD,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,GAAG,IAAIV,IACjEJ,KACAM;IAGF,IAAIQ,WAAWX,WAAWW,MAAM,EAAE;QAChC,MAAM,qBAAoE,CAApE,IAAIC,MAAM,CAAC,iDAAiD,EAAEf,KAAK,GAAnE,qBAAA;mBAAA;wBAAA;0BAAA;QAAmE;IAC3E;IAEA,OAAO;QACLU;QACAM,OAAOd,aAAaJ,uBAAuBa,gBAAgBM;QAC3DL;QACAC;QACAJ,MAAMA,KAAKS,KAAK,CAACJ,OAAOK,MAAM;QAC9B,gFAAgF;QAChF,qCAAqC;QACrCC,SAASH;IACX;AACF","ignoreList":[0]}
+22
View File
@@ -0,0 +1,22 @@
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);
return {
hash: parsedURL.hash,
hostname: parsedURL.hostname,
href: parsedURL.href,
pathname: parsedURL.pathname,
port: parsedURL.port,
protocol: parsedURL.protocol,
query: searchParamsToUrlQuery(parsedURL.searchParams),
search: parsedURL.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 hash: string\n hostname?: string | null\n href: string\n pathname: string\n port?: string | null\n protocol?: string | null\n query: ParsedUrlQuery\n origin?: string | null\n search: string\n slashes: boolean | undefined\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 return {\n hash: parsedURL.hash,\n hostname: parsedURL.hostname,\n href: parsedURL.href,\n pathname: parsedURL.pathname,\n port: parsedURL.port,\n protocol: parsedURL.protocol,\n query: searchParamsToUrlQuery(parsedURL.searchParams),\n search: parsedURL.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","hash","hostname","href","pathname","port","protocol","query","searchParams","search","origin","slashes","slice","length"],"mappings":"AAEA,SAASA,sBAAsB,QAAQ,gBAAe;AACtD,SAASC,gBAAgB,QAAQ,uBAAsB;AAevD,OAAO,SAASC,SAASC,GAAW;IAClC,IAAIA,IAAIC,UAAU,CAAC,MAAM;QACvB,OAAOH,iBAAiBE;IAC1B;IAEA,MAAME,YAAY,IAAIC,IAAIH;IAC1B,OAAO;QACLI,MAAMF,UAAUE,IAAI;QACpBC,UAAUH,UAAUG,QAAQ;QAC5BC,MAAMJ,UAAUI,IAAI;QACpBC,UAAUL,UAAUK,QAAQ;QAC5BC,MAAMN,UAAUM,IAAI;QACpBC,UAAUP,UAAUO,QAAQ;QAC5BC,OAAOb,uBAAuBK,UAAUS,YAAY;QACpDC,QAAQV,UAAUU,MAAM;QACxBC,QAAQX,UAAUW,MAAM;QACxBC,SACEZ,UAAUI,IAAI,CAACS,KAAK,CAClBb,UAAUO,QAAQ,CAACO,MAAM,EACzBd,UAAUO,QAAQ,CAACO,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
+200
View File
@@ -0,0 +1,200 @@
/**
* 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 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