Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
57a66c4
docs(next-drupal)!: more detailed instruction for local dev & contrib…
yobottehg Dec 6, 2024
1793ceb
feat(next-drupal)!: upgrade to next.js 15 and react 19
yobottehg Dec 6, 2024
8f52387
feat(next-drupal)!: upgrade to next.js 15 and react 19
yobottehg Dec 6, 2024
ef3f8f8
feat(basic-starter)!: upgrade to next.js 15 and react 19
yobottehg Dec 6, 2024
7c9ffeb
feat(pages-starter)!: upgrade to next.js 15 and react 19
yobottehg Dec 6, 2024
08ed20b
feat(graphql-starter)!: upgrade to next.js 15 and react 19
yobottehg Dec 6, 2024
fea2caf
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
4c30157
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
c437af0
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
cd4085a
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
8802691
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
e85ffb1
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
60257e5
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
87d38a5
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
2f40d7e
fix(next-drupal)!: upgrade tests to new async request objects
yobottehg Dec 6, 2024
62fad2d
fix(next-drupal)!: replace bogus draftModeStores
yobottehg Dec 6, 2024
ff9c22c
revert link related changes, PR reviews for draft and cookie store, a…
yobottehg Jan 20, 2025
94ca5ba
revert bogus change
yobottehg Jan 20, 2025
e48f97c
revert bogus change
yobottehg Jan 20, 2025
3e0fb3d
fix(next-drupal): make tests pass by re-assigning draftMode variable
yobottehg Jan 20, 2025
8168a73
fix(next-drupal): use link component again, draft mode convention
yobottehg Jan 24, 2025
07a2b7b
fix(next-drupal): sorting
yobottehg Jan 24, 2025
36d157f
Merge branch 'main' into next_15_support
yobottehg Jan 24, 2025
2823757
fix(next-drupal, *-starter, www): disallow next.js versions with secu…
yobottehg Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ yarn workspace next-drupal dev

You can run all the tests from the root of the repository.

Due to the current CI/CD setup it is not possible to run the tests locally.
A running Drupal instance with a secret configuration is required to run the tests.

To add the required env vars to Jest run:

```
cp packages/next-drupal/.env.example packages/next-drupal/.env
```

### `next-drupal`

We use `jest` for testing the `next-drupal` package.
Expand Down
5 changes: 5 additions & 0 deletions packages/next-drupal/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DRUPAL_BASE_URL=http://localhost
DRUPAL_USERNAME=drupal
DRUPAL_PASSWORD=drupal
DRUPAL_CLIENT_ID=next-drupal
DRUPAL_CLIENT_SECRET=next-drupal
12 changes: 6 additions & 6 deletions packages/next-drupal/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "next-drupal",
"description": "Helpers for Next.js + Drupal.",
"version": "2.0.0-beta.1",
"version": "2.0.0-beta.2",
"sideEffects": false,
"source": [
"src/index.ts",
Expand Down Expand Up @@ -73,13 +73,13 @@
},
"dependencies": {
"jsona": "^1.12.1",
"next": "^13.4 || ^14",
"next": "^15.0.4",
"node-cache": "^5.1.2",
"qs": "^6.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"qs": "^6.13.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5.4.5"
"typescript": "^5.7.2"
}
}
27 changes: 16 additions & 11 deletions packages/next-drupal/src/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ export async function enableDraftMode(
const searchParams = request.nextUrl.searchParams
const path = searchParams.get("path")

const cookieStore = await cookies()
// Enable Draft Mode by setting the cookie
draftMode().enable()
;(await draftMode()).enable()

// Override the default SameSite=lax.
// See https://github.com/vercel/next.js/issues/49927
const draftModeCookie = cookies().get(DRAFT_MODE_COOKIE_NAME)
const draftModeCookie = cookieStore.get(DRAFT_MODE_COOKIE_NAME)
if (draftModeCookie) {
cookies().set({
cookieStore.set({
...draftModeCookie,
sameSite: "none",
secure: true,
Expand All @@ -41,7 +42,7 @@ export async function enableDraftMode(
const { secret, scope, plugin, ...draftData } = Object.fromEntries(
searchParams.entries()
)
cookies().set({
cookieStore.set({
...draftModeCookie,
name: DRAFT_DATA_COOKIE_NAME,
sameSite: "none",
Expand All @@ -54,9 +55,10 @@ export async function enableDraftMode(
redirect(path)
}

export function disableDraftMode() {
cookies().delete(DRAFT_DATA_COOKIE_NAME)
draftMode().disable()
export async function disableDraftMode() {
const cookieStore = await cookies()
cookieStore.delete(DRAFT_DATA_COOKIE_NAME)
;(await draftMode()).disable()

return new Response("Draft mode is disabled")
}
Expand All @@ -66,11 +68,14 @@ export interface DraftData {
resourceVersion?: string
}

export function getDraftData() {
export async function getDraftData() {
let data: DraftData = {}

if (draftMode().isEnabled && cookies().has(DRAFT_DATA_COOKIE_NAME)) {
data = JSON.parse(cookies().get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
const cookieStore = await cookies()
if (
(await draftMode()).isEnabled &&
cookieStore.has(DRAFT_DATA_COOKIE_NAME)
) {
data = JSON.parse(cookieStore.get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
}

return data
Expand Down
96 changes: 50 additions & 46 deletions packages/next-drupal/tests/draft/draft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
getDraftData,
} from "../../src/draft"
import { resetNextHeaders } from "../__mocks__/next/headers"
import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"
import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"

jest.mock("next/headers")
jest.mock("next/navigation", () => ({
Expand Down Expand Up @@ -62,7 +62,7 @@ describe("enableDraftMode()", () => {

const response = await enableDraftMode(request, drupal)

expect(draftMode().enable).not.toHaveBeenCalled()
expect((await draftMode()).enable).not.toHaveBeenCalled()
expect(response).toBeInstanceOf(Response)
expect(response.status).toBe(500)
})
Expand All @@ -72,30 +72,31 @@ describe("enableDraftMode()", () => {

await enableDraftMode(request, drupal)

expect(draftMode().enable).toHaveBeenCalled()
expect((await draftMode()).enable).toHaveBeenCalled()
})

test("updates draft mode cookie’s sameSite flag", async () => {
spyOnFetch({ responseBody: validationPayload })

// Our mock draftMode().enable does not set a cookie, so we set one.
cookies().set(draftModeCookie)
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax")
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBeFalsy()
;(await cookies()).set(draftModeCookie)

expect((await cookies()).get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax")
expect((await cookies()).get(DRAFT_MODE_COOKIE_NAME).secure).toBeFalsy()

await enableDraftMode(request, drupal)

expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none")
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBe(true)
expect((await cookies()).get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none")
expect((await cookies()).get(DRAFT_MODE_COOKIE_NAME).secure).toBe(true)
})

test("sets a draft data cookie", async () => {
spyOnFetch({ responseBody: validationPayload })
expect(cookies().get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined)
expect((await cookies()).get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined)

await enableDraftMode(request, drupal)

const cookie = cookies().get(DRAFT_DATA_COOKIE_NAME)
const cookie = (await cookies()).get(DRAFT_DATA_COOKIE_NAME)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { secret, plugin, ...data } = Object.fromEntries(
searchParams.entries()
Expand All @@ -118,25 +119,28 @@ describe("enableDraftMode()", () => {
})

describe("disableDraftMode()", () => {
test("draft data cookie was deleted", () => {
disableDraftMode()
test("draft data cookie was deleted", async () => {
await disableDraftMode()

expect(cookies).toHaveBeenCalledTimes(1)
expect(cookies().delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect((await cookies()).delete).toHaveBeenCalledWith(
DRAFT_DATA_COOKIE_NAME
)
})

test("draft mode was disabled", () => {
test("draft mode was disabled", async () => {
// First ensure draft mode is enabled.
draftMode().enable()
expect(draftMode().isEnabled).toBe(true)

disableDraftMode()
expect(draftMode().disable).toHaveBeenCalledTimes(1)
expect(draftMode().isEnabled).toBe(false)
;(await draftMode()).enable()
expect((await draftMode()).isEnabled).toBe(true)

await disableDraftMode()
expect((await draftMode()).disable).toHaveBeenCalledTimes(1)
expect((await draftMode()).isEnabled).toBe(false)
})

test("returns a response object", async () => {
const response = disableDraftMode()
const response = await disableDraftMode()

expect(response).toBeInstanceOf(Response)
expect(response.ok).toBe(true)
Expand All @@ -156,51 +160,51 @@ describe("getDraftData()", () => {
secure: true,
}

test("returns empty object if draft mode disabled", () => {
cookies().set(draftDataCookie)
test("returns empty object if draft mode disabled", async () => {
;(await cookies()).set(draftDataCookie)

const data = getDraftData()
expect(draftMode().isEnabled).toBe(false)
expect(cookies().has).toHaveBeenCalledTimes(0)
expect(cookies().get).toHaveBeenCalledTimes(0)
const data = await getDraftData()
expect((await draftMode()).isEnabled).toBe(false)
expect((await cookies()).has).toHaveBeenCalledTimes(0)
expect((await cookies()).get).toHaveBeenCalledTimes(0)
expect(data).toMatchObject({})
})

test("returns empty object if no draft data cookie", () => {
draftMode().enable()
test("returns empty object if no draft data cookie", async () => {
;(await draftMode()).enable()
draftMode.mockClear()

const data = getDraftData()
const data = await getDraftData()
expect(draftMode).toHaveBeenCalledTimes(1)
expect(draftMode().isEnabled).toBe(true)
expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookies().has).toHaveBeenCalledTimes(1)
expect(cookies().get).toHaveBeenCalledTimes(0)
expect((await draftMode()).isEnabled).toBe(true)
expect((await cookies()).has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect((await cookies()).has).toHaveBeenCalledTimes(1)
expect((await cookies()).get).toHaveBeenCalledTimes(0)
expect(data).toMatchObject({})
})

test("returns empty object if no draft data cookie value", () => {
cookies().set({
test("returns empty object if no draft data cookie value", async () => {
;(await cookies()).set({
...draftDataCookie,
value: "",
})
draftMode().enable()
;(await draftMode()).enable()
draftMode.mockClear()

const data = getDraftData()
const data = await getDraftData()
expect(draftMode).toHaveBeenCalledTimes(1)
expect(draftMode().isEnabled).toBe(true)
expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookies().has).toHaveBeenCalledTimes(1)
expect(cookies().get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookies().get).toHaveBeenCalledTimes(1)
expect((await draftMode()).isEnabled).toBe(true)
expect((await cookies()).has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect((await cookies()).has).toHaveBeenCalledTimes(1)
expect((await cookies()).get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect((await cookies()).get).toHaveBeenCalledTimes(1)
expect(data).toMatchObject({})
})

test("returns the JSON.parse()d data", () => {
cookies().set(draftDataCookie)
draftMode().enable()
test("returns the JSON.parse()d data", async () => {
;(await cookies()).set(draftDataCookie)
;(await draftMode()).enable()

expect(getDraftData()).toMatchObject(draftData)
expect(await getDraftData()).toMatchObject(draftData)
})
})
2 changes: 1 addition & 1 deletion packages/next-drupal/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "node_modules/next/types/*.d.ts"]
}
21 changes: 13 additions & 8 deletions starters/basic-starter/app/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function getNode(slug: string[]) {

const params: JsonApiParams = {}

const draftData = getDraftData()
const draftData = await getDraftData()

if (draftData.path === path) {
params.resourceVersion = draftData.resourceVersion
Expand Down Expand Up @@ -60,13 +60,17 @@ type NodePageParams = {
}
type NodePageProps = {
params: NodePageParams
searchParams: { [key: string]: string | string[] | undefined }
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
{ params: { slug } }: NodePageProps,
props: NodePageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const params = await props.params

const { slug } = params

let node
try {
node = await getNode(slug)
Expand Down Expand Up @@ -108,11 +112,12 @@ export async function generateStaticParams(): Promise<NodePageParams[]> {
})
}

export default async function NodePage({
params: { slug },
searchParams,
}: NodePageProps) {
const isDraftMode = draftMode().isEnabled
export default async function NodePage(props: NodePageProps) {
const params = await props.params

const { slug } = params

const isDraftMode = (await draftMode()).isEnabled

let node
try {
Expand Down
4 changes: 2 additions & 2 deletions starters/basic-starter/app/api/disable-draft/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { disableDraftMode } from "next-drupal/draft"
import type { NextRequest } from "next/server"

export async function GET(request: NextRequest) {
return disableDraftMode()
export async function GET(_: NextRequest) {
return await disableDraftMode()
}
2 changes: 1 addition & 1 deletion starters/basic-starter/app/api/draft/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { enableDraftMode } from "next-drupal/draft"
import type { NextRequest } from "next/server"

export async function GET(request: NextRequest): Promise<Response | never> {
return enableDraftMode(request, drupal)
return await enableDraftMode(request, drupal)
}
4 changes: 2 additions & 2 deletions starters/basic-starter/components/misc/DraftAlert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Suspense } from "react"
import { draftMode } from "next/headers"
import { DraftAlertClient } from "./Client"

export function DraftAlert() {
const isDraftEnabled = draftMode().isEnabled
export async function DraftAlert() {
const isDraftEnabled = (await draftMode()).isEnabled

return (
<Suspense fallback={null}>
Expand Down
Loading
Loading