Skip to content

Commit

Permalink
feat: simplify normalization logic
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Nov 7, 2023
1 parent e65d4a1 commit c3ebc99
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 320 deletions.
80 changes: 41 additions & 39 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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) {
Expand Down Expand Up @@ -475,14 +475,28 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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()
Expand Down Expand Up @@ -567,7 +581,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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
Expand All @@ -578,7 +592,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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
Expand Down Expand Up @@ -958,32 +972,18 @@ export default abstract class Server<ServerOptions extends Options = Options> {
'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
Expand All @@ -998,7 +998,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
addRequestMeta(req, 'postponed', postponed)
}

matchedPath = this.stripNextDataPath(matchedPath, false)
matchedPath = this.normalize(matchedPath)
const normalizedUrlPath = this.stripNextDataPath(urlPathname)

// Perform locale detection and normalization.
Expand Down Expand Up @@ -1422,18 +1422,20 @@ export default abstract class Server<ServerOptions extends Options = Options> {
private normalize = (pathname: string) => {
const normalizers: Array<PathnameNormalizer> = []

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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
})
})
36 changes: 10 additions & 26 deletions packages/next/src/server/future/normalizers/request/base-path.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
31 changes: 11 additions & 20 deletions packages/next/src/server/future/normalizers/request/next-data.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,39 @@ 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)
}
})

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)
}
})
})

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)
Expand All @@ -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(
'/'
)
Expand Down
Loading

0 comments on commit c3ebc99

Please sign in to comment.