From 045ad0d133c92ba38998226a015fcb2df89bc6ff Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 6 Apr 2023 13:18:42 -0600 Subject: [PATCH] Replaced Reflect with ReflectAdapter (#48000) On some runtimes, `Reflect` is not available. This creates a new "naive" implementation. --- packages/next/src/server/render.tsx | 14 ++++---- .../spec-extension/adapters/headers.test.ts | 3 ++ .../web/spec-extension/adapters/headers.ts | 28 +++++++++------ .../web/spec-extension/adapters/reflect.ts | 34 +++++++++++++++++++ .../adapters/request-cookies.ts | 5 +-- test/e2e/app-dir/app-routes/helpers.ts | 5 ++- test/lib/next-modes/next-deploy.ts | 2 +- 7 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 packages/next/src/server/web/spec-extension/adapters/reflect.ts diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index e876d4ddf0c29..a95cbf3bd589e 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -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 @@ -979,7 +980,7 @@ export async function renderToHTML( let deferredContent = false if (process.env.NODE_ENV !== 'production') { resOrProxy = new Proxy(res, { - get: function (obj, prop, receiver) { + get: function (obj, prop) { if (!canAccessRes) { const message = `You should not access 'res' after getServerSideProps resolves.` + @@ -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) }, }) } diff --git a/packages/next/src/server/web/spec-extension/adapters/headers.test.ts b/packages/next/src/server/web/spec-extension/adapters/headers.test.ts index 8eccef7bebbc6..0ebddea2c7ff6 100644 --- a/packages/next/src/server/web/spec-extension/adapters/headers.test.ts +++ b/packages/next/src/server/web/spec-extension/adapters/headers.test.ts @@ -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') diff --git a/packages/next/src/server/web/spec-extension/adapters/headers.ts b/packages/next/src/server/web/spec-extension/adapters/headers.ts index 8b325515c4804..c5e9d2bd35a4f 100644 --- a/packages/next/src/server/web/spec-extension/adapters/headers.ts +++ b/packages/next/src/server/web/spec-extension/adapters/headers.ts @@ -1,5 +1,7 @@ import type { IncomingHttpHeaders } from 'http' +import { ReflectAdapter } from './reflect' + /** * @internal */ @@ -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() @@ -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() @@ -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() @@ -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() @@ -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) }, }) } @@ -107,7 +113,7 @@ export class HeadersAdapter extends Headers { * any mutating method is called. */ public static seal(headers: Headers): ReadonlyHeaders { - return new Proxy(headers, { + return new Proxy(headers, { get(target, prop, receiver) { switch (prop) { case 'append': @@ -115,7 +121,7 @@ export class HeadersAdapter extends Headers { case 'set': return ReadonlyHeadersError.callable default: - return Reflect.get(target, prop, receiver) + return ReflectAdapter.get(target, prop, receiver) } }, }) diff --git a/packages/next/src/server/web/spec-extension/adapters/reflect.ts b/packages/next/src/server/web/spec-extension/adapters/reflect.ts new file mode 100644 index 0000000000000..b933b2557a928 --- /dev/null +++ b/packages/next/src/server/web/spec-extension/adapters/reflect.ts @@ -0,0 +1,34 @@ +export class ReflectAdapter { + static get( + 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( + target: T, + prop: string | symbol, + value: any, + receiver: any + ): boolean { + return Reflect.set(target, prop, value, receiver) + } + + static has(target: T, prop: string | symbol): boolean { + return Reflect.has(target, prop) + } + + static deleteProperty( + target: T, + prop: string | symbol + ): boolean { + return Reflect.deleteProperty(target, prop) + } +} diff --git a/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts b/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts index 89dd9bf458363..69639823f105a 100644 --- a/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts +++ b/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts @@ -1,4 +1,5 @@ import type { RequestCookies } from '../cookies' +import { ReflectAdapter } from './reflect' /** * @internal @@ -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) } }, }) diff --git a/test/e2e/app-dir/app-routes/helpers.ts b/test/e2e/app-dir/app-routes/helpers.ts index 08739f879b5fe..c87f0b3e3d07f 100644 --- a/test/e2e/app-dir/app-routes/helpers.ts +++ b/test/e2e/app-dir/app-routes/helpers.ts @@ -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' /** @@ -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 { const headerOrCookie = headersOrCookies.get(KEY) if (!headerOrCookie) return {} diff --git a/test/lib/next-modes/next-deploy.ts b/test/lib/next-modes/next-deploy.ts index a2b5349daae2d..094770f81b585 100644 --- a/test/lib/next-modes/next-deploy.ts +++ b/test/lib/next-modes/next-deploy.ts @@ -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,