diff --git a/packages/next/src/client/components/draft-mode.ts b/packages/next/src/client/components/draft-mode.ts new file mode 100644 index 000000000000..b1af9a4c2d7b --- /dev/null +++ b/packages/next/src/client/components/draft-mode.ts @@ -0,0 +1,28 @@ +import { DraftModeProvider } from '../../server/async-storage/draft-mode-provider' +import { staticGenerationBailout } from './static-generation-bailout' + +export class DraftMode { + /** + * @internal - this declaration is stripped via `tsc --stripInternal` + */ + private readonly _provider: DraftModeProvider + + constructor(provider: DraftModeProvider) { + this._provider = provider + } + get isEnabled() { + return this._provider.isEnabled + } + public enable() { + if (staticGenerationBailout('draftMode().enable()')) { + return + } + return this._provider.enable() + } + public disable() { + if (staticGenerationBailout('draftMode().disable()')) { + return + } + return this._provider.disable() + } +} diff --git a/packages/next/src/client/components/headers.ts b/packages/next/src/client/components/headers.ts index 388d768d2067..2b239c9b8763 100644 --- a/packages/next/src/client/components/headers.ts +++ b/packages/next/src/client/components/headers.ts @@ -7,7 +7,7 @@ import { RequestCookies } from '../../server/web/spec-extension/cookies' import { requestAsyncStorage } from './request-async-storage' import { actionAsyncStorage } from './action-async-storage' import { staticGenerationBailout } from './static-generation-bailout' -import { DraftMode } from '../../../types' +import { DraftMode } from './draft-mode' export function headers() { if (staticGenerationBailout('headers')) { @@ -49,28 +49,12 @@ export function cookies() { return requestStore.cookies } -export function draftMode(): DraftMode { +export function draftMode() { const requestStore = requestAsyncStorage.getStore() if (!requestStore) { throw new Error( `Invariant: Method expects to have requestAsyncStorage, none available` ) } - return { - get enabled() { - return requestStore.draftMode.enabled - }, - enable() { - if (staticGenerationBailout('draftMode().enable()')) { - return - } - return requestStore.draftMode.enable() - }, - disable() { - if (staticGenerationBailout('draftMode().disable()')) { - return - } - return requestStore.draftMode.disable() - }, - } + return new DraftMode(requestStore.draftMode) } diff --git a/packages/next/src/client/components/request-async-storage.ts b/packages/next/src/client/components/request-async-storage.ts index d2879e12c6b0..a8a71c790662 100644 --- a/packages/next/src/client/components/request-async-storage.ts +++ b/packages/next/src/client/components/request-async-storage.ts @@ -1,6 +1,6 @@ import type { AsyncLocalStorage } from 'async_hooks' +import type { DraftModeProvider } from '../../server/async-storage/draft-mode-provider' import type { ResponseCookies } from '../../server/web/spec-extension/cookies' -import type { DraftMode } from '../../../types' import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers' import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies' @@ -10,7 +10,7 @@ export interface RequestStore { readonly headers: ReadonlyHeaders readonly cookies: ReadonlyRequestCookies readonly mutableCookies: ResponseCookies - readonly draftMode: DraftMode + readonly draftMode: DraftModeProvider } export type RequestAsyncStorage = AsyncLocalStorage diff --git a/packages/next/src/server/async-storage/draft-mode.ts b/packages/next/src/server/async-storage/draft-mode-provider.ts similarity index 70% rename from packages/next/src/server/async-storage/draft-mode.ts rename to packages/next/src/server/async-storage/draft-mode-provider.ts index e3f3f82b8fdc..06138db7dd16 100644 --- a/packages/next/src/server/async-storage/draft-mode.ts +++ b/packages/next/src/server/async-storage/draft-mode-provider.ts @@ -10,16 +10,24 @@ import { __ApiPreviewProps, } from '../api-utils' -export class DraftMode { - public readonly enabled: boolean +export class DraftModeProvider { + public readonly isEnabled: boolean - private readonly previewModeId: string | undefined + /** + * @internal - this declaration is stripped via `tsc --stripInternal` + */ + private readonly _previewModeId: string | undefined + + /** + * @internal - this declaration is stripped via `tsc --stripInternal` + */ + private readonly _mutableCookies: ResponseCookies constructor( previewProps: __ApiPreviewProps | undefined, req: IncomingMessage | BaseNextRequest | NextRequest, - private readonly cookies: ReadonlyRequestCookies, - private readonly mutableCookies: ResponseCookies + cookies: ReadonlyRequestCookies, + mutableCookies: ResponseCookies ) { // The logic for draftMode() is very similar to tryGetPreviewData() // but Draft Mode does not have any data associated with it. @@ -27,28 +35,29 @@ export class DraftMode { previewProps && checkIsOnDemandRevalidate(req, previewProps).isOnDemandRevalidate - const cookieValue = this.cookies.get(COOKIE_NAME_PRERENDER_BYPASS)?.value + const cookieValue = cookies.get(COOKIE_NAME_PRERENDER_BYPASS)?.value - this.enabled = Boolean( + this.isEnabled = Boolean( !isOnDemandRevalidate && cookieValue && previewProps && cookieValue === previewProps.previewModeId ) - this.previewModeId = previewProps?.previewModeId + this._previewModeId = previewProps?.previewModeId + this._mutableCookies = mutableCookies } enable() { - if (!this.previewModeId) { + if (!this._previewModeId) { throw new Error( - 'Invariant: previewProps missing previewModeId this should not be hit' + 'Invariant: previewProps missing previewModeId this should never happen' ) } - this.mutableCookies.set({ + this._mutableCookies.set({ name: COOKIE_NAME_PRERENDER_BYPASS, - value: this.previewModeId, + value: this._previewModeId, httpOnly: true, sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax', secure: process.env.NODE_ENV !== 'development', @@ -60,7 +69,7 @@ export class DraftMode { // To delete a cookie, set `expires` to a date in the past: // https://tools.ietf.org/html/rfc6265#section-4.1.1 // `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted. - this.mutableCookies.set({ + this._mutableCookies.set({ name: COOKIE_NAME_PRERENDER_BYPASS, value: '', httpOnly: true, diff --git a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts index 860de534258a..4d91f7f77806 100644 --- a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts @@ -18,7 +18,7 @@ import { } from '../web/spec-extension/adapters/request-cookies' import { RequestCookies, ResponseCookies } from '../web/spec-extension/cookies' import { __ApiPreviewProps } from '../api-utils' -import { DraftMode } from './draft-mode' +import { DraftModeProvider } from './draft-mode-provider' function getHeaders(headers: Headers | IncomingHttpHeaders): ReadonlyHeaders { const cleaned = HeadersAdapter.from(headers) @@ -79,7 +79,7 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< headers?: ReadonlyHeaders cookies?: ReadonlyRequestCookies mutableCookies?: ResponseCookies - draftMode?: DraftMode + draftMode?: DraftModeProvider } = {} const store: RequestStore = { @@ -109,7 +109,7 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< }, get draftMode() { if (!cache.draftMode) { - cache.draftMode = new DraftMode( + cache.draftMode = new DraftModeProvider( previewProps, req, this.cookies, diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 9942b6c11842..297e169cd2b7 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -141,22 +141,6 @@ export { export type PreviewData = string | false | object | undefined -export type DraftMode = { - /** - * Get the current value of Draft Mode. - * True when enabled, false when disabled. - */ - enabled: boolean - /** - * Set the value of Draft Mode to true. - */ - enable: () => void - /** - * Set the value of Draft Mode to false. - */ - disable: () => void -} - /** * Context object passed into `getStaticProps`. * @link https://nextjs.org/docs/api-reference/data-fetching/get-static-props#context-parameter diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 58c6f510c91d..a0e1f8a6759c 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -1356,7 +1356,7 @@ createNextDescribe( .elementByCss('#draft-mode') .text() - expect(content).toBe('{"result":{"enabled":false}}') + expect(content).toBe('{"isEnabled":false}') }) it('should force SSR correctly for headers usage', async () => { diff --git a/test/e2e/app-dir/app-static/app/ssg-draft-mode/[[...route]]/page.js b/test/e2e/app-dir/app-static/app/ssg-draft-mode/[[...route]]/page.js index 13a7ce2660f6..ad91464d34d6 100644 --- a/test/e2e/app-dir/app-static/app/ssg-draft-mode/[[...route]]/page.js +++ b/test/e2e/app-dir/app-static/app/ssg-draft-mode/[[...route]]/page.js @@ -1,11 +1,11 @@ import { draftMode } from 'next/headers' export default function Page() { - const result = draftMode() + const { isEnabled } = draftMode() return (
-
{JSON.stringify({ result })}
+
{JSON.stringify({ isEnabled })}
) } diff --git a/test/e2e/app-dir/draft-mode/app/page.tsx b/test/e2e/app-dir/draft-mode/app/page.tsx index aa408718d721..65b59b64e32a 100644 --- a/test/e2e/app-dir/draft-mode/app/page.tsx +++ b/test/e2e/app-dir/draft-mode/app/page.tsx @@ -2,7 +2,7 @@ import React from 'react' import { draftMode } from 'next/headers' export default function Page() { - const { enabled } = draftMode() + const { isEnabled } = draftMode() return ( <> @@ -11,7 +11,7 @@ export default function Page() { Random: {Math.random()}

- State: {enabled ? 'ENABLED' : 'DISABLED'} + State: {isEnabled ? 'ENABLED' : 'DISABLED'}

) diff --git a/test/e2e/app-dir/draft-mode/app/state/route.ts b/test/e2e/app-dir/draft-mode/app/state/route.ts index c4081345262d..c5568ed06b89 100644 --- a/test/e2e/app-dir/draft-mode/app/state/route.ts +++ b/test/e2e/app-dir/draft-mode/app/state/route.ts @@ -1,6 +1,6 @@ import { draftMode } from 'next/headers' export function GET() { - const { enabled } = draftMode() - return new Response(enabled ? 'ENABLED' : 'DISABLED') + const { isEnabled } = draftMode() + return new Response(isEnabled ? 'ENABLED' : 'DISABLED') } diff --git a/test/e2e/app-dir/draft-mode/app/with-edge/page.tsx b/test/e2e/app-dir/draft-mode/app/with-edge/page.tsx index 84b5fb6f099d..bdcd910fa793 100644 --- a/test/e2e/app-dir/draft-mode/app/with-edge/page.tsx +++ b/test/e2e/app-dir/draft-mode/app/with-edge/page.tsx @@ -4,7 +4,7 @@ import { draftMode } from 'next/headers' export const runtime = 'experimental-edge' export default function Page() { - const { enabled } = draftMode() + const { isEnabled } = draftMode() return ( <> @@ -13,7 +13,7 @@ export default function Page() { Random: {Math.random()}

- State: {enabled ? 'ENABLED' : 'DISABLED'} + State: {isEnabled ? 'ENABLED' : 'DISABLED'}

) diff --git a/test/e2e/app-dir/draft-mode/app/with-edge/state/route.ts b/test/e2e/app-dir/draft-mode/app/with-edge/state/route.ts index 50426a2ec42c..04083518e8b6 100644 --- a/test/e2e/app-dir/draft-mode/app/with-edge/state/route.ts +++ b/test/e2e/app-dir/draft-mode/app/with-edge/state/route.ts @@ -3,6 +3,6 @@ import { draftMode } from 'next/headers' export const runtime = 'edge' export function GET() { - const { enabled } = draftMode() - return new Response(enabled ? 'ENABLED' : 'DISABLED') + const { isEnabled } = draftMode() + return new Response(isEnabled ? 'ENABLED' : 'DISABLED') } diff --git a/test/e2e/app-dir/hooks/app/hooks/use-draft-mode/page.js b/test/e2e/app-dir/hooks/app/hooks/use-draft-mode/page.js index 141d1afc7320..bf91e2ee399f 100644 --- a/test/e2e/app-dir/hooks/app/hooks/use-draft-mode/page.js +++ b/test/e2e/app-dir/hooks/app/hooks/use-draft-mode/page.js @@ -1,7 +1,7 @@ import { draftMode } from 'next/headers' export default function Page() { - const { enabled } = draftMode() + const { isEnabled } = draftMode() return ( <> @@ -9,7 +9,7 @@ export default function Page() {

Rand: {Math.random()}

-

{enabled ? 'ENABLED' : 'DISABLED'}

+

{isEnabled ? 'ENABLED' : 'DISABLED'}

) }