From b562d0d7e12d5ddc5f422830f59c042333a04f27 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 7 Nov 2023 10:38:55 -0700 Subject: [PATCH] feat: simplify normalization logic --- packages/next/src/server/base-server.ts | 80 +++++++++--------- .../normalizers/request/base-path.test.ts | 50 +---------- .../future/normalizers/request/base-path.ts | 36 +++----- .../future/normalizers/request/next-data.ts | 31 +++---- .../normalizers/request/postponed.test.ts | 40 ++------- .../future/normalizers/request/postponed.ts | 30 +++---- .../normalizers/request/prefetch-rsc.test.ts | 82 ------------------- .../normalizers/request/prefetch-rsc.ts | 23 ++---- .../future/normalizers/request/prefix.test.ts | 46 +++++++++++ .../future/normalizers/request/prefix.ts | 31 +++++++ .../server/future/normalizers/request/rsc.ts | 23 ++---- .../request/{rsc.test.ts => suffix.test.ts} | 28 ++----- 12 files changed, 180 insertions(+), 320 deletions(-) delete mode 100644 packages/next/src/server/future/normalizers/request/prefetch-rsc.test.ts create mode 100644 packages/next/src/server/future/normalizers/request/prefix.test.ts create mode 100644 packages/next/src/server/future/normalizers/request/prefix.ts rename packages/next/src/server/future/normalizers/request/{rsc.test.ts => suffix.test.ts} (55%) diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 1ac898e6baa1b..262f2cf0e3cb7 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -405,10 +405,10 @@ export default abstract class Server { protected readonly localeNormalizer?: LocaleRouteNormalizer protected readonly normalizers: { - readonly postponed: PostponedPathnameNormalizer - readonly rsc: RSCPathnameNormalizer - readonly prefetchRSC: PrefetchRSCPathnameNormalizer - readonly data: NextDataPathnameNormalizer + readonly postponed: PostponedPathnameNormalizer | undefined + readonly rsc: RSCPathnameNormalizer | undefined + readonly prefetchRSC: PrefetchRSCPathnameNormalizer | undefined + readonly data: NextDataPathnameNormalizer | undefined } public constructor(options: ServerOptions) { @@ -475,14 +475,28 @@ export default abstract class Server { this.enabledDirectories = this.getEnabledDirectories(dev) this.normalizers = { - postponed: new PostponedPathnameNormalizer( - this.enabledDirectories.app && this.nextConfig.experimental.ppr - ), - rsc: new RSCPathnameNormalizer(this.enabledDirectories.app), - prefetchRSC: new PrefetchRSCPathnameNormalizer( - this.enabledDirectories.app - ), - data: new NextDataPathnameNormalizer(this.buildId), + // We should normalize the pathname from the RSC prefix only in minimal + // mode as otherwise that route is not exposed external to the server as + // we instead only rely on the headers. + postponed: + this.enabledDirectories.app && + this.nextConfig.experimental.ppr && + this.minimalMode + ? new PostponedPathnameNormalizer() + : undefined, + rsc: + this.enabledDirectories.app && this.minimalMode + ? new RSCPathnameNormalizer() + : undefined, + prefetchRSC: + this.enabledDirectories.app && + this.nextConfig.experimental.ppr && + this.minimalMode + ? new PrefetchRSCPathnameNormalizer() + : undefined, + data: this.enabledDirectories.pages + ? new NextDataPathnameNormalizer(this.buildId) + : undefined, } this.nextFontManifest = this.getNextFontManifest() @@ -567,7 +581,7 @@ export default abstract class Server { private handleRSCRequest: RouteHandler = (req, _res, parsedUrl) => { if (!parsedUrl.pathname) return false - if (this.normalizers.prefetchRSC.match(parsedUrl.pathname)) { + if (this.normalizers.prefetchRSC?.match(parsedUrl.pathname)) { parsedUrl.pathname = this.normalizers.prefetchRSC.normalize( parsedUrl.pathname, true @@ -578,7 +592,7 @@ export default abstract class Server { req.headers[NEXT_ROUTER_PREFETCH_HEADER.toLowerCase()] = '1' addRequestMeta(req, 'isRSCRequest', true) addRequestMeta(req, 'isPrefetchRSCRequest', true) - } else if (this.normalizers.rsc.match(parsedUrl.pathname)) { + } else if (this.normalizers.rsc?.match(parsedUrl.pathname)) { parsedUrl.pathname = this.normalizers.rsc.normalize( parsedUrl.pathname, true @@ -958,32 +972,18 @@ export default abstract class Server { 'http://localhost' ) - if (this.normalizers.rsc.match(matchedPath)) { - matchedPath = this.normalizers.rsc.normalize(matchedPath, true) - } else if (this.normalizers.postponed.match(matchedPath)) { - matchedPath = this.normalizers.postponed.normalize( - matchedPath, - true - ) - } - const { pathname: urlPathname } = new URL(req.url, 'http://localhost') // For ISR the URL is normalized to the prerenderPath so if // it's a data request the URL path will be the data URL, // basePath is already stripped by this point - if ( - this.enabledDirectories.pages && - this.normalizers.data.match(urlPathname) - ) { + if (this.normalizers.data?.match(urlPathname)) { parsedUrl.query.__nextDataReq = '1' } // In minimal mode, if PPR is enabled, then we should check to see if // the matched path is a postponed path, and if it is, handle it. else if ( - this.minimalMode && - this.renderOpts.experimental.ppr && - this.normalizers.postponed.match(matchedPath) && + this.normalizers.postponed?.match(matchedPath) && req.method === 'POST' ) { // Decode the postponed state from the request body, it will come as @@ -998,7 +998,7 @@ export default abstract class Server { addRequestMeta(req, 'postponed', postponed) } - matchedPath = this.stripNextDataPath(matchedPath, false) + matchedPath = this.normalize(matchedPath) const normalizedUrlPath = this.stripNextDataPath(urlPathname) // Perform locale detection and normalization. @@ -1422,18 +1422,20 @@ export default abstract class Server { private normalize = (pathname: string) => { const normalizers: Array = [] - if (this.enabledDirectories.pages) { + if (this.normalizers.data) { normalizers.push(this.normalizers.data) } - if (this.minimalMode && this.enabledDirectories.app) { - if (this.renderOpts.experimental.ppr) { - normalizers.push(this.normalizers.postponed) - // We have to put the prefetch normalizer before the RSC normalizer - // because the RSC normalizer will match the prefetch RSC routes too. - normalizers.push(this.normalizers.prefetchRSC) - } + // NOTE: we don't normalize for postponed here as it only applies to the + // `x-matched-path` header in minimal mode + + // We have to put the prefetch normalizer before the RSC normalizer + // because the RSC normalizer will match the prefetch RSC routes too. + if (this.normalizers.prefetchRSC) { + normalizers.push(this.normalizers.prefetchRSC) + } + if (this.normalizers.rsc) { normalizers.push(this.normalizers.rsc) } diff --git a/packages/next/src/server/future/normalizers/request/base-path.test.ts b/packages/next/src/server/future/normalizers/request/base-path.test.ts index 48eb3858992cc..f7663775d3181 100644 --- a/packages/next/src/server/future/normalizers/request/base-path.test.ts +++ b/packages/next/src/server/future/normalizers/request/base-path.test.ts @@ -1,53 +1,11 @@ import { BasePathPathnameNormalizer } from './base-path' describe('BasePathPathnameNormalizer', () => { - describe('match', () => { - it('should return false if there is no basePath', () => { - let normalizer = new BasePathPathnameNormalizer('') - expect(normalizer.match('/')).toBe(false) - normalizer = new BasePathPathnameNormalizer('/') - expect(normalizer.match('/')).toBe(false) - }) - - it('should return false if the pathname does not start with the basePath', () => { - const normalizer = new BasePathPathnameNormalizer('/foo') - const pathnames = ['/bar', '/bar/foo', '/fooo/bar'] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) - - it('should return true if the pathname starts with the basePath', () => { - const normalizer = new BasePathPathnameNormalizer('/foo') - const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(true) - } - }) + it('should throw when provided with a blank basePath', () => { + expect(() => new BasePathPathnameNormalizer('')).toThrow() }) - describe('normalize', () => { - it('should return the same pathname if there is no basePath', () => { - let normalizer = new BasePathPathnameNormalizer('') - expect(normalizer.normalize('/foo')).toBe('/foo') - normalizer = new BasePathPathnameNormalizer('/') - expect(normalizer.normalize('/foo')).toBe('/foo') - }) - - it('should return the same pathname if we are not matched and the pathname does not start with the basePath', () => { - const normalizer = new BasePathPathnameNormalizer('/foo') - let pathnames = ['/bar', '/bar/foo', '/fooo/bar'] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname) - } - }) - - it('should strip the basePath from the pathname when it matches', () => { - const normalizer = new BasePathPathnameNormalizer('/foo') - const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname.substring(4)) - } - }) + it('should throw when provided with a basePath of "/"', () => { + expect(() => new BasePathPathnameNormalizer('/')).toThrow() }) }) diff --git a/packages/next/src/server/future/normalizers/request/base-path.ts b/packages/next/src/server/future/normalizers/request/base-path.ts index 046fbdf8d4d74..41b184193da8d 100644 --- a/packages/next/src/server/future/normalizers/request/base-path.ts +++ b/packages/next/src/server/future/normalizers/request/base-path.ts @@ -1,32 +1,16 @@ import type { PathnameNormalizer } from './pathname-normalizer' -export class BasePathPathnameNormalizer implements PathnameNormalizer { - private readonly basePath?: string - constructor(basePath: string) { - // A basePath of `/` is not a basePath. - if (!basePath || basePath === '/') return - - this.basePath = basePath - } - - public match(pathname: string) { - // If there's no basePath, we don't match. - if (!this.basePath) return false - - // If the pathname doesn't start with the basePath, we don't match. - if (pathname !== this.basePath && !pathname.startsWith(this.basePath + '/')) - return false +import { PrefixPathnameNormalizer } from './prefix' - return true - } - - public normalize(pathname: string, matched?: boolean): string { - // If there's no basePath, we don't need to normalize. - if (!this.basePath) return pathname - - // If we're not matched and we don't match, we don't need to normalize. - if (!matched && !this.match(pathname)) return pathname +export class BasePathPathnameNormalizer + extends PrefixPathnameNormalizer + implements PathnameNormalizer +{ + constructor(basePath: string) { + if (!basePath || basePath === '/') { + throw new Error('Invariant: basePath must be set and cannot be "/"') + } - return pathname.substring(this.basePath.length) + super(basePath) } } diff --git a/packages/next/src/server/future/normalizers/request/next-data.ts b/packages/next/src/server/future/normalizers/request/next-data.ts index 332bb8718d96b..240bff9a4f0e4 100644 --- a/packages/next/src/server/future/normalizers/request/next-data.ts +++ b/packages/next/src/server/future/normalizers/request/next-data.ts @@ -1,40 +1,31 @@ import type { PathnameNormalizer } from './pathname-normalizer' +import { denormalizePagePath } from '../../../../shared/lib/page-path/denormalize-page-path' +import { PrefixPathnameNormalizer } from './prefix' +import { SuffixPathnameNormalizer } from './suffix' + export class NextDataPathnameNormalizer implements PathnameNormalizer { - private readonly prefix: string + private readonly prefix: PrefixPathnameNormalizer + private readonly suffix = new SuffixPathnameNormalizer('.json') constructor(buildID: string) { if (!buildID) { throw new Error('Invariant: buildID is required') } - this.prefix = `/_next/data/${buildID}` + this.prefix = new PrefixPathnameNormalizer(`/_next/data/${buildID}`) } public match(pathname: string) { - // If the pathname doesn't start with the prefix, we don't match. - if (!pathname.startsWith(`${this.prefix}/`)) return false - - // If the pathname ends with `.json`, we don't match. - if (!pathname.endsWith('.json')) return false - - return true + return this.prefix.match(pathname) && this.suffix.match(pathname) } public normalize(pathname: string, matched?: boolean): string { // If we're not matched and we don't match, we don't need to normalize. if (!matched && !this.match(pathname)) return pathname - // Remove the prefix and the `.json` extension. - pathname = pathname.substring( - this.prefix.length, - pathname.length - '.json'.length - ) - - // If the pathname is `/index`, we normalize it to `/`. - if (pathname === '/index') { - return '/' - } + pathname = this.prefix.normalize(pathname, true) + pathname = this.suffix.normalize(pathname, true) - return pathname + return denormalizePagePath(pathname) } } diff --git a/packages/next/src/server/future/normalizers/request/postponed.test.ts b/packages/next/src/server/future/normalizers/request/postponed.test.ts index 7b87e046050a8..0d4d881b24618 100644 --- a/packages/next/src/server/future/normalizers/request/postponed.test.ts +++ b/packages/next/src/server/future/normalizers/request/postponed.test.ts @@ -2,25 +2,13 @@ import { PostponedPathnameNormalizer } from './postponed' describe('PostponedPathnameNormalizer', () => { describe('match', () => { - it('should not match if it is disabled', () => { + it('should match', () => { const pathnames = [ '/_next/postponed/resume/foo', '/_next/postponed/resume/bar', '/_next/postponed/resume/baz', ] - const normalizer = new PostponedPathnameNormalizer(false) - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) - - it('should match if it is enabled', () => { - const pathnames = [ - '/_next/postponed/resume/foo', - '/_next/postponed/resume/bar', - '/_next/postponed/resume/baz', - ] - const normalizer = new PostponedPathnameNormalizer(true) + const normalizer = new PostponedPathnameNormalizer() for (const pathname of pathnames) { expect(normalizer.match(pathname)).toBe(true) } @@ -28,7 +16,7 @@ describe('PostponedPathnameNormalizer', () => { it('should not match for other pathnames', () => { const pathnames = ['/_next/foo', '/_next/bar', '/_next/baz'] - const normalizer = new PostponedPathnameNormalizer(true) + const normalizer = new PostponedPathnameNormalizer() for (const pathname of pathnames) { expect(normalizer.match(pathname)).toBe(false) } @@ -36,29 +24,17 @@ describe('PostponedPathnameNormalizer', () => { }) describe('normalize', () => { - it('should not normalize if it is disabled', () => { - const pathnames = [ - '/_next/postponed/resume/foo', - '/_next/postponed/resume/bar', - '/_next/postponed/resume/baz', - ] - const normalizer = new PostponedPathnameNormalizer(false) - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname) - } - }) - - it('should not normalize if it is enabled but not matched', () => { + it('should not normalize but not matched', () => { const pathnames = ['/_next/foo', '/_next/bar', '/_next/baz'] - const normalizer = new PostponedPathnameNormalizer(true) + const normalizer = new PostponedPathnameNormalizer() for (const pathname of pathnames) { expect(normalizer.normalize(pathname)).toBe(pathname) } }) - it('should normalize if it is enabled and matched', () => { + it('should normalize and matched', () => { const pathnames = ['/foo', '/bar', '/baz'] - const normalizer = new PostponedPathnameNormalizer(true) + const normalizer = new PostponedPathnameNormalizer() for (const pathname of pathnames) { expect( normalizer.normalize(`/_next/postponed/resume${pathname}`, true) @@ -67,7 +43,7 @@ describe('PostponedPathnameNormalizer', () => { }) it('should normalize `/index` to `/`', () => { - const normalizer = new PostponedPathnameNormalizer(true) + const normalizer = new PostponedPathnameNormalizer() expect(normalizer.normalize('/_next/postponed/resume/index', true)).toBe( '/' ) diff --git a/packages/next/src/server/future/normalizers/request/postponed.ts b/packages/next/src/server/future/normalizers/request/postponed.ts index 173771a296924..9c95654806f72 100644 --- a/packages/next/src/server/future/normalizers/request/postponed.ts +++ b/packages/next/src/server/future/normalizers/request/postponed.ts @@ -1,33 +1,25 @@ +import { denormalizePagePath } from '../../../../shared/lib/page-path/denormalize-page-path' import type { PathnameNormalizer } from './pathname-normalizer' -const prefix = '/_next/postponed/resume' - -export class PostponedPathnameNormalizer implements PathnameNormalizer { - constructor(private readonly ppr: boolean | undefined) {} - - public match(pathname: string) { - // If PPR isn't enabled, we don't match. - if (!this.ppr) return false +import { PrefixPathnameNormalizer } from './prefix' - // If the pathname doesn't start with the prefix, we don't match. - if (!pathname.startsWith(prefix)) return false +const prefix = '/_next/postponed/resume' - return true +export class PostponedPathnameNormalizer + extends PrefixPathnameNormalizer + implements PathnameNormalizer +{ + constructor() { + super(prefix) } public normalize(pathname: string, matched?: boolean): string { - // If PPR isn't enabled, we don't need to normalize. - if (!this.ppr) return pathname - // If we're not matched and we don't match, we don't need to normalize. if (!matched && !this.match(pathname)) return pathname // Remove the prefix. - pathname = pathname.substring(prefix.length) || '/' - - // If the pathname is equal to `/index`, we normalize it to `/`. - if (pathname === '/index') return '/' + pathname = super.normalize(pathname, true) - return pathname + return denormalizePagePath(pathname) } } diff --git a/packages/next/src/server/future/normalizers/request/prefetch-rsc.test.ts b/packages/next/src/server/future/normalizers/request/prefetch-rsc.test.ts deleted file mode 100644 index 4d956f070d7ec..0000000000000 --- a/packages/next/src/server/future/normalizers/request/prefetch-rsc.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PrefetchRSCPathnameNormalizer } from './prefetch-rsc' - -describe('PrefetchRSCPathnameNormalizer', () => { - describe('match', () => { - it('should return false if the pathname does not end with `.prefetch.rsc`', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(true) - const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) - - it('should return true if it matches', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(true) - const pathnames = [ - '/foo.prefetch.rsc', - '/foo/bar.prefetch.rsc', - '/fooo/bar.prefetch.rsc', - ] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(true) - } - }) - - it('should return false if it is disabled but ends with .prefetch.rsc', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(false) - const pathnames = [ - '/foo.prefetch.rsc', - '/foo/bar.prefetch.rsc', - '/fooo/bar.prefetch.rsc', - ] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) - - it('should return false if it only ends in .rsc', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(false) - const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) - }) - - describe('normalize', () => { - it('should return the same pathname if we are not matched and the pathname does not end with `.prefetch.rsc`', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(true) - const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname) - } - }) - - it('should strip the `.prefetch.rsc` extension from the pathname when it matches', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(true) - const pathnames = [ - '/foo.prefetch.rsc', - '/foo/bar.prefetch.rsc', - '/fooo/bar.prefetch.rsc', - ] - const expected = ['/foo', '/foo/bar', '/fooo/bar'] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe( - expected[pathnames.indexOf(pathname)] - ) - } - }) - - it('should return the same pathname if it is disabled but ends with .prefetch.rsc', () => { - const normalizer = new PrefetchRSCPathnameNormalizer(false) - const pathnames = [ - '/foo.prefetch.rsc', - '/foo/bar.prefetch.rsc', - '/fooo/bar.prefetch.rsc', - ] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname) - } - }) - }) -}) diff --git a/packages/next/src/server/future/normalizers/request/prefetch-rsc.ts b/packages/next/src/server/future/normalizers/request/prefetch-rsc.ts index fe95dbabb2d5e..650f165e07419 100644 --- a/packages/next/src/server/future/normalizers/request/prefetch-rsc.ts +++ b/packages/next/src/server/future/normalizers/request/prefetch-rsc.ts @@ -3,22 +3,11 @@ import type { PathnameNormalizer } from './pathname-normalizer' import { RSC_PREFETCH_SUFFIX } from '../../../../lib/constants' import { SuffixPathnameNormalizer } from './suffix' -export class PrefetchRSCPathnameNormalizer implements PathnameNormalizer { - private readonly suffix = new SuffixPathnameNormalizer(RSC_PREFETCH_SUFFIX) - - constructor(private readonly hasAppDir: boolean) {} - - public match(pathname: string) { - // If there's no app directory, we don't match. - if (!this.hasAppDir) return false - - return this.suffix.match(pathname) - } - - public normalize(pathname: string, matched?: boolean): string { - // If there's no app directory, we don't need to normalize. - if (!this.hasAppDir) return pathname - - return this.suffix.normalize(pathname, matched) +export class PrefetchRSCPathnameNormalizer + extends SuffixPathnameNormalizer + implements PathnameNormalizer +{ + constructor() { + super(RSC_PREFETCH_SUFFIX) } } diff --git a/packages/next/src/server/future/normalizers/request/prefix.test.ts b/packages/next/src/server/future/normalizers/request/prefix.test.ts new file mode 100644 index 0000000000000..afc396c5d08b4 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/prefix.test.ts @@ -0,0 +1,46 @@ +import { PrefixPathnameNormalizer } from './prefix' + +describe('PrefixPathnameNormalizer', () => { + describe('match', () => { + it('should return false if the pathname does not start with the prefix', () => { + const normalizer = new PrefixPathnameNormalizer('/foo') + const pathnames = ['/bar', '/bar/foo', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should return true if the pathname starts with the prefix', () => { + const normalizer = new PrefixPathnameNormalizer('/foo') + const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(true) + } + }) + }) + + it('should throw if the prefix ends with a slash', () => { + expect(() => new PrefixPathnameNormalizer('/foo/')).toThrow() + expect(() => new PrefixPathnameNormalizer('/')).toThrow() + }) + + describe('normalize', () => { + it('should return the same pathname if we are not matched and the pathname does not start with the prefix', () => { + const normalizer = new PrefixPathnameNormalizer('/foo') + let pathnames = ['/bar', '/bar/foo', '/fooo/bar'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should strip the prefix from the pathname when it matches', () => { + const normalizer = new PrefixPathnameNormalizer('/foo') + const pathnames = ['/foo', '/foo/bar', '/foo/bar/baz'] + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe( + pathname.substring(4) || '/' + ) + } + }) + }) +}) diff --git a/packages/next/src/server/future/normalizers/request/prefix.ts b/packages/next/src/server/future/normalizers/request/prefix.ts new file mode 100644 index 0000000000000..f8c3423e62214 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/prefix.ts @@ -0,0 +1,31 @@ +import type { Normalizer } from '../normalizer' + +export class PrefixPathnameNormalizer implements Normalizer { + constructor(private readonly prefix: string) { + if (prefix.endsWith('/')) { + throw new Error( + `PrefixPathnameNormalizer: prefix "${prefix}" should not end with a slash` + ) + } + } + + public match(pathname: string) { + // If the pathname doesn't start with the prefix, we don't match. + if (pathname !== this.prefix && !pathname.startsWith(this.prefix + '/')) { + return false + } + + return true + } + + public normalize(pathname: string, matched?: boolean): string { + // If we're not matched and we don't match, we don't need to normalize. + if (!matched && !this.match(pathname)) return pathname + + if (pathname.length === this.prefix.length) { + return '/' + } + + return pathname.substring(this.prefix.length) + } +} diff --git a/packages/next/src/server/future/normalizers/request/rsc.ts b/packages/next/src/server/future/normalizers/request/rsc.ts index da80a066f416f..65becee9f9749 100644 --- a/packages/next/src/server/future/normalizers/request/rsc.ts +++ b/packages/next/src/server/future/normalizers/request/rsc.ts @@ -3,22 +3,11 @@ import type { PathnameNormalizer } from './pathname-normalizer' import { RSC_SUFFIX } from '../../../../lib/constants' import { SuffixPathnameNormalizer } from './suffix' -export class RSCPathnameNormalizer implements PathnameNormalizer { - private readonly suffix = new SuffixPathnameNormalizer(RSC_SUFFIX) - - constructor(private readonly hasAppDir: boolean) {} - - public match(pathname: string) { - // If there's no app directory, we don't match. - if (!this.hasAppDir) return false - - return this.suffix.match(pathname) - } - - public normalize(pathname: string, matched?: boolean): string { - // If there's no app directory, we don't need to normalize. - if (!this.hasAppDir) return pathname - - return this.suffix.normalize(pathname, matched) +export class RSCPathnameNormalizer + extends SuffixPathnameNormalizer + implements PathnameNormalizer +{ + constructor() { + super(RSC_SUFFIX) } } diff --git a/packages/next/src/server/future/normalizers/request/rsc.test.ts b/packages/next/src/server/future/normalizers/request/suffix.test.ts similarity index 55% rename from packages/next/src/server/future/normalizers/request/rsc.test.ts rename to packages/next/src/server/future/normalizers/request/suffix.test.ts index 5ed8c13ff9019..dd6b3a8a1a451 100644 --- a/packages/next/src/server/future/normalizers/request/rsc.test.ts +++ b/packages/next/src/server/future/normalizers/request/suffix.test.ts @@ -1,9 +1,9 @@ -import { RSCPathnameNormalizer } from './rsc' +import { SuffixPathnameNormalizer } from './suffix' -describe('RSCPathnameNormalizer', () => { +describe('SuffixPathnameNormalizer', () => { describe('match', () => { it('should return false if the pathname does not end with `.rsc`', () => { - const normalizer = new RSCPathnameNormalizer(true) + const normalizer = new SuffixPathnameNormalizer('.rsc') const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] for (const pathname of pathnames) { expect(normalizer.match(pathname)).toBe(false) @@ -11,25 +11,17 @@ describe('RSCPathnameNormalizer', () => { }) it('should return true if it matches', () => { - const normalizer = new RSCPathnameNormalizer(true) + const normalizer = new SuffixPathnameNormalizer('.rsc') const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] for (const pathname of pathnames) { expect(normalizer.match(pathname)).toBe(true) } }) - - it('should return false if it is disabled but ends with .rsc', () => { - const normalizer = new RSCPathnameNormalizer(false) - const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] - for (const pathname of pathnames) { - expect(normalizer.match(pathname)).toBe(false) - } - }) }) describe('normalize', () => { it('should return the same pathname if we are not matched and the pathname does not end with `.rsc`', () => { - const normalizer = new RSCPathnameNormalizer(true) + const normalizer = new SuffixPathnameNormalizer('.rsc') const pathnames = ['/foo', '/foo/bar', '/fooo/bar'] for (const pathname of pathnames) { expect(normalizer.normalize(pathname)).toBe(pathname) @@ -37,7 +29,7 @@ describe('RSCPathnameNormalizer', () => { }) it('should strip the `.rsc` extension from the pathname when it matches', () => { - const normalizer = new RSCPathnameNormalizer(true) + const normalizer = new SuffixPathnameNormalizer('.rsc') const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] const expected = ['/foo', '/foo/bar', '/fooo/bar'] for (const pathname of pathnames) { @@ -46,13 +38,5 @@ describe('RSCPathnameNormalizer', () => { ) } }) - - it('should return the same pathname if it is disabled but ends with .rsc', () => { - const normalizer = new RSCPathnameNormalizer(false) - const pathnames = ['/foo.rsc', '/foo/bar.rsc', '/fooo/bar.rsc'] - for (const pathname of pathnames) { - expect(normalizer.normalize(pathname)).toBe(pathname) - } - }) }) })