Skip to content

Commit

Permalink
Replaced Reflect with ReflectAdapter (#48000)
Browse files Browse the repository at this point in the history
On some runtimes, `Reflect` is not available. This creates a new "naive"
implementation.
  • Loading branch information
wyattjoh authored Apr 6, 2023
1 parent 9448913 commit 045ad0d
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 23 deletions.
14 changes: 6 additions & 8 deletions packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import { SearchParamsContext } from '../shared/lib/hooks-client-context'
import { getTracer } from './lib/trace/tracer'
import { RenderSpan } from './lib/trace/constants'
import { PageNotFoundError } from '../shared/lib/utils'
import { ReflectAdapter } from './web/spec-extension/adapters/reflect'

let tryGetPreviewData: typeof import('./api-utils/node').tryGetPreviewData
let warn: typeof import('../build/output/log').warn
Expand Down Expand Up @@ -979,7 +980,7 @@ export async function renderToHTML(
let deferredContent = false
if (process.env.NODE_ENV !== 'production') {
resOrProxy = new Proxy<ServerResponse>(res, {
get: function (obj, prop, receiver) {
get: function (obj, prop) {
if (!canAccessRes) {
const message =
`You should not access 'res' after getServerSideProps resolves.` +
Expand All @@ -991,15 +992,12 @@ export async function renderToHTML(
warn(message)
}
}
const value = Reflect.get(obj, prop, receiver)

// since ServerResponse uses internal fields which
// proxy can't map correctly we need to ensure functions
// are bound correctly while being proxied
if (typeof value === 'function') {
return value.bind(obj)
if (typeof prop === 'symbol') {
return ReflectAdapter.get(obj, prop, res)
}
return value

return ReflectAdapter.get(obj, prop, res)
},
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ describe('HeadersAdapter', () => {
const sealed = HeadersAdapter.seal(headers)
expect(sealed).toBeInstanceOf(Headers)

expect(sealed.get('content-type')).toBe('application/json')
expect(sealed.get('x-custom-header')).toBe('custom')

// These methods are not available on the sealed instance
expect(() =>
(sealed as any).append('x-custom-header', 'custom2')
Expand Down
28 changes: 17 additions & 11 deletions packages/next/src/server/web/spec-extension/adapters/headers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IncomingHttpHeaders } from 'http'

import { ReflectAdapter } from './reflect'

/**
* @internal
*/
Expand Down Expand Up @@ -30,7 +32,9 @@ export class HeadersAdapter extends Headers {
// Because this is just an object, we expect that all "get" operations
// are for properties. If it's a "get" for a symbol, we'll just return
// the symbol.
if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver)
if (typeof prop === 'symbol') {
return ReflectAdapter.get(target, prop, receiver)
}

const lowercased = prop.toLowerCase()

Expand All @@ -45,10 +49,12 @@ export class HeadersAdapter extends Headers {
if (typeof original === 'undefined') return

// If the original casing exists, return the value.
return Reflect.get(target, original, receiver)
return ReflectAdapter.get(target, original, receiver)
},
set(target, prop, value) {
if (typeof prop === 'symbol') return Reflect.set(target, prop, value)
set(target, prop, value, receiver) {
if (typeof prop === 'symbol') {
return ReflectAdapter.set(target, prop, value, receiver)
}

const lowercased = prop.toLowerCase()

Expand All @@ -60,10 +66,10 @@ export class HeadersAdapter extends Headers {
)

// If the original casing doesn't exist, use the prop as the key.
return Reflect.set(target, original ?? prop, value)
return ReflectAdapter.set(target, original ?? prop, value, receiver)
},
has(target, prop) {
if (typeof prop === 'symbol') return Reflect.has(target, prop)
if (typeof prop === 'symbol') return ReflectAdapter.has(target, prop)

const lowercased = prop.toLowerCase()

Expand All @@ -78,11 +84,11 @@ export class HeadersAdapter extends Headers {
if (typeof original === 'undefined') return false

// If the original casing exists, return true.
return Reflect.has(target, original)
return ReflectAdapter.has(target, original)
},
deleteProperty(target, prop) {
if (typeof prop === 'symbol')
return Reflect.deleteProperty(target, prop)
return ReflectAdapter.deleteProperty(target, prop)

const lowercased = prop.toLowerCase()

Expand All @@ -97,7 +103,7 @@ export class HeadersAdapter extends Headers {
if (typeof original === 'undefined') return true

// If the original casing exists, delete the property.
return Reflect.deleteProperty(target, original)
return ReflectAdapter.deleteProperty(target, original)
},
})
}
Expand All @@ -107,15 +113,15 @@ export class HeadersAdapter extends Headers {
* any mutating method is called.
*/
public static seal(headers: Headers): ReadonlyHeaders {
return new Proxy(headers, {
return new Proxy<ReadonlyHeaders>(headers, {
get(target, prop, receiver) {
switch (prop) {
case 'append':
case 'delete':
case 'set':
return ReadonlyHeadersError.callable
default:
return Reflect.get(target, prop, receiver)
return ReflectAdapter.get(target, prop, receiver)
}
},
})
Expand Down
34 changes: 34 additions & 0 deletions packages/next/src/server/web/spec-extension/adapters/reflect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export class ReflectAdapter {
static get<T extends object>(
target: T,
prop: string | symbol,
receiver: unknown
): any {
const value = Reflect.get(target, prop, receiver)
if (typeof value === 'function') {
return value.bind(target)
}

return value
}

static set<T extends object>(
target: T,
prop: string | symbol,
value: any,
receiver: any
): boolean {
return Reflect.set(target, prop, value, receiver)
}

static has<T extends object>(target: T, prop: string | symbol): boolean {
return Reflect.has(target, prop)
}

static deleteProperty<T extends object>(
target: T,
prop: string | symbol
): boolean {
return Reflect.deleteProperty(target, prop)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RequestCookies } from '../cookies'
import { ReflectAdapter } from './reflect'

/**
* @internal
Expand All @@ -23,14 +24,14 @@ export type ReadonlyRequestCookies = Omit<
export class RequestCookiesAdapter {
public static seal(cookies: RequestCookies): ReadonlyRequestCookies {
return new Proxy(cookies, {
get(target, prop) {
get(target, prop, receiver) {
switch (prop) {
case 'clear':
case 'delete':
case 'set':
return ReadonlyRequestCookiesError.callable
default:
return Reflect.get(target, prop)
return ReflectAdapter.get(target, prop, receiver)
}
},
})
Expand Down
5 changes: 4 additions & 1 deletion test/e2e/app-dir/app-routes/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { ReadonlyHeaders } from 'next/dist/server/web/spec-extension/adapters/headers'
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies'

const KEY = 'x-request-meta'

/**
Expand Down Expand Up @@ -55,7 +58,7 @@ type Cookies = {
* @returns any injected metadata on the request
*/
export function getRequestMeta(
headersOrCookies: Headers | Cookies
headersOrCookies: Headers | Cookies | ReadonlyHeaders | ReadonlyRequestCookies
): Record<string, any> {
const headerOrCookie = headersOrCookies.get(KEY)
if (!headerOrCookie) return {}
Expand Down
2 changes: 1 addition & 1 deletion test/lib/next-modes/next-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class NextDeployInstance extends NextInstance {
// link the project
const linkRes = await execa(
'vercel',
['link', '-p', TEST_PROJECT_NAME, '--confirm', ...vercelFlags],
['link', '-p', TEST_PROJECT_NAME, '--yes', ...vercelFlags],
{
cwd: this.testDir,
env: vercelEnv,
Expand Down

0 comments on commit 045ad0d

Please sign in to comment.