// TODO: fix the no-non-null assertion errors /* eslint-disable @typescript-eslint/no-non-null-assertion */ /** * This class provides a logic of sorting dynamic routes in NextJS. * * It was copied from * @see https://github.com/vercel/next.js/blob/canary/packages/next/shared/lib/router/utils/sorted-routes.ts */ export class UrlNode { placeholder = true; children: Map = new Map(); slugName: string | null = null; restSlugName: string | null = null; optionalRestSlugName: string | null = null; insert(urlPath: string): void { this._insert(urlPath.split('/').filter(Boolean), [], false); } smoosh(): string[] { return this._smoosh(); } private _smoosh(prefix = '/'): string[] { 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 new Error( `You cannot define a route with the same specificity as a optional catch-all route ("${r}" and "${r}[[...${this.optionalRestSlugName}]]").`, ); } 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; } private _insert( urlPaths: string[], slugNames: string[], isCatchAll: boolean, ): void { if (urlPaths.length === 0) { this.placeholder = false; return; } if (isCatchAll) { throw new Error(`Catch-all must be the last part of the URL.`); } // 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('...')) { // Strip `...`, leaving only `something` segmentName = segmentName.substring(3); isCatchAll = true; } if (segmentName.startsWith('[') || segmentName.endsWith(']')) { throw new Error( `Segment names may not start or end with extra brackets ('${segmentName}').`, ); } if (segmentName.startsWith('.')) { throw new Error( `Segment names may not start with erroneous periods ('${segmentName}').`, ); } const handleSlug = function handleSlug( previousSlug: string | null, nextSlug: string, ) { if (previousSlug !== null && previousSlug !== nextSlug) { throw new Error( `You cannot use different slug names for the same dynamic path ('${previousSlug}' !== '${nextSlug}').`, ); } slugNames.forEach((slug) => { if (slug === nextSlug) { throw new Error( `You cannot have the same slug name "${nextSlug}" repeat within a single dynamic path`, ); } if (slug.replace(/\W/g, '') === nextSegment.replace(/\W/g, '')) { throw new Error( `You cannot have the slug names "${slug}" and "${nextSlug}" differ only by non-word symbols within a single dynamic path`, ); } }); slugNames.push(nextSlug); }; if (isCatchAll) { if (isOptional) { if (this.restSlugName != null) { throw new Error( `You cannot use both an required and optional catch-all route at the same level ("[...${this.restSlugName}]" and "${urlPaths[0]}" ).`, ); } 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 new Error( `You cannot use both an optional and required catch-all route at the same level ("[[...${this.optionalRestSlugName}]]" and "${urlPaths[0]}").`, ); } 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 new Error( `Optional route parameters are not yet supported ("${urlPaths[0]}").`, ); } 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); } }