Skip to content

Commit 2246df1

Browse files
authored
feat(next-drupal, starters-*) - Add Next 15 support (#824)
1 parent 6178aeb commit 2246df1

File tree

19 files changed

+3638
-3317
lines changed

19 files changed

+3638
-3317
lines changed

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ yarn workspace next-drupal dev
5050

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

53+
Due to the current CI/CD setup it is not possible to run the tests locally.
54+
A running Drupal instance with a secret configuration is required to run the tests.
55+
56+
To add the required env vars to Jest run:
57+
58+
```
59+
cp packages/next-drupal/.env.example packages/next-drupal/.env
60+
```
61+
5362
### `next-drupal`
5463

5564
We use `jest` for testing the `next-drupal` package.

packages/next-drupal/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DRUPAL_BASE_URL=http://localhost
2+
DRUPAL_USERNAME=drupal
3+
DRUPAL_PASSWORD=drupal
4+
DRUPAL_CLIENT_ID=next-drupal
5+
DRUPAL_CLIENT_SECRET=next-drupal

packages/next-drupal/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "next-drupal",
33
"description": "Helpers for Next.js + Drupal.",
4-
"version": "2.0.0-beta.1",
4+
"version": "2.0.0-beta.2",
55
"sideEffects": false,
66
"source": [
77
"src/index.ts",
@@ -73,13 +73,13 @@
7373
},
7474
"dependencies": {
7575
"jsona": "^1.12.1",
76-
"next": "^13.4 || ^14",
76+
"next": "^14.2.21 || ^15.1.2",
7777
"node-cache": "^5.1.2",
78-
"qs": "^6.12.1",
79-
"react": "^18.2.0",
80-
"react-dom": "^18.2.0"
78+
"qs": "^6.13.1",
79+
"react": "^18.2 || ^19.0",
80+
"react-dom": "^18.2 || ^19.0"
8181
},
8282
"devDependencies": {
83-
"typescript": "^5.4.5"
83+
"typescript": "^5.7.2"
8484
}
8585
}

packages/next-drupal/src/draft.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ export async function enableDraftMode(
2222
const searchParams = request.nextUrl.searchParams
2323
const path = searchParams.get("path")
2424

25+
const cookieStore = await cookies()
2526
// Enable Draft Mode by setting the cookie
26-
draftMode().enable()
27+
const draft = await draftMode()
28+
draft.enable()
2729

2830
// Override the default SameSite=lax.
2931
// See https://github.com/vercel/next.js/issues/49927
30-
const draftModeCookie = cookies().get(DRAFT_MODE_COOKIE_NAME)
32+
const draftModeCookie = cookieStore.get(DRAFT_MODE_COOKIE_NAME)
3133
if (draftModeCookie) {
32-
cookies().set({
34+
cookieStore.set({
3335
...draftModeCookie,
3436
sameSite: "none",
3537
secure: true,
@@ -41,7 +43,7 @@ export async function enableDraftMode(
4143
const { secret, scope, plugin, ...draftData } = Object.fromEntries(
4244
searchParams.entries()
4345
)
44-
cookies().set({
46+
cookieStore.set({
4547
...draftModeCookie,
4648
name: DRAFT_DATA_COOKIE_NAME,
4749
sameSite: "none",
@@ -54,9 +56,11 @@ export async function enableDraftMode(
5456
redirect(path)
5557
}
5658

57-
export function disableDraftMode() {
58-
cookies().delete(DRAFT_DATA_COOKIE_NAME)
59-
draftMode().disable()
59+
export async function disableDraftMode() {
60+
const cookieStore = await cookies()
61+
cookieStore.delete(DRAFT_DATA_COOKIE_NAME)
62+
const draft = await draftMode()
63+
draft.disable()
6064

6165
return new Response("Draft mode is disabled")
6266
}
@@ -66,11 +70,12 @@ export interface DraftData {
6670
resourceVersion?: string
6771
}
6872

69-
export function getDraftData() {
73+
export async function getDraftData() {
7074
let data: DraftData = {}
71-
72-
if (draftMode().isEnabled && cookies().has(DRAFT_DATA_COOKIE_NAME)) {
73-
data = JSON.parse(cookies().get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
75+
const cookieStore = await cookies()
76+
const draft = await draftMode()
77+
if (draft.isEnabled && cookieStore.has(DRAFT_DATA_COOKIE_NAME)) {
78+
data = JSON.parse(cookieStore.get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
7479
}
7580

7681
return data

packages/next-drupal/tests/draft/draft.test.ts

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
getDraftData,
2222
} from "../../src/draft"
2323
import { resetNextHeaders } from "../__mocks__/next/headers"
24-
import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"
24+
import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"
2525

2626
jest.mock("next/headers")
2727
jest.mock("next/navigation", () => ({
@@ -51,6 +51,7 @@ describe("enableDraftMode()", () => {
5151
`https://example.com/api/draft?${searchParams}`
5252
)
5353
const drupal = new NextDrupalBase(BASE_URL)
54+
5455
const draftModeCookie: ResponseCookie = {
5556
name: DRAFT_MODE_COOKIE_NAME,
5657
value: "some-secret-key",
@@ -62,7 +63,8 @@ describe("enableDraftMode()", () => {
6263

6364
const response = await enableDraftMode(request, drupal)
6465

65-
expect(draftMode().enable).not.toHaveBeenCalled()
66+
const draft = await draftMode()
67+
expect(draft.enable).not.toHaveBeenCalled()
6668
expect(response).toBeInstanceOf(Response)
6769
expect(response.status).toBe(500)
6870
})
@@ -71,31 +73,35 @@ describe("enableDraftMode()", () => {
7173
spyOnFetch({ responseBody: validationPayload })
7274

7375
await enableDraftMode(request, drupal)
74-
75-
expect(draftMode().enable).toHaveBeenCalled()
76+
const draft = await draftMode()
77+
expect(draft.enable).toHaveBeenCalled()
7678
})
7779

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

83+
const cookieStore = await cookies()
8184
// Our mock draftMode().enable does not set a cookie, so we set one.
82-
cookies().set(draftModeCookie)
83-
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax")
84-
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBeFalsy()
85+
cookieStore.set(draftModeCookie)
86+
87+
expect(cookieStore.get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax")
88+
expect(cookieStore.get(DRAFT_MODE_COOKIE_NAME).secure).toBeFalsy()
8589

8690
await enableDraftMode(request, drupal)
8791

88-
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none")
89-
expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBe(true)
92+
expect(cookieStore.get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none")
93+
expect(cookieStore.get(DRAFT_MODE_COOKIE_NAME).secure).toBe(true)
9094
})
9195

9296
test("sets a draft data cookie", async () => {
9397
spyOnFetch({ responseBody: validationPayload })
94-
expect(cookies().get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined)
98+
const cookieStore = await cookies()
99+
100+
expect(cookieStore.get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined)
95101

96102
await enableDraftMode(request, drupal)
97103

98-
const cookie = cookies().get(DRAFT_DATA_COOKIE_NAME)
104+
const cookie = cookieStore.get(DRAFT_DATA_COOKIE_NAME)
99105
// eslint-disable-next-line @typescript-eslint/no-unused-vars
100106
const { secret, plugin, ...data } = Object.fromEntries(
101107
searchParams.entries()
@@ -118,25 +124,31 @@ describe("enableDraftMode()", () => {
118124
})
119125

120126
describe("disableDraftMode()", () => {
121-
test("draft data cookie was deleted", () => {
122-
disableDraftMode()
123-
124-
expect(cookies).toHaveBeenCalledTimes(1)
125-
expect(cookies().delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
127+
test("draft data cookie was deleted", async () => {
128+
const cookieStore = await cookies()
129+
await disableDraftMode()
130+
expect(cookies).toHaveBeenCalledTimes(2)
131+
expect(cookieStore.delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
126132
})
127133

128-
test("draft mode was disabled", () => {
129-
// First ensure draft mode is enabled.
130-
draftMode().enable()
131-
expect(draftMode().isEnabled).toBe(true)
134+
test("draft mode was disabled", async () => {
135+
let draft = await draftMode()
136+
draft.enable()
132137

133-
disableDraftMode()
134-
expect(draftMode().disable).toHaveBeenCalledTimes(1)
135-
expect(draftMode().isEnabled).toBe(false)
138+
// Re-assign draftMode because local variable is not updated
139+
draft = await draftMode()
140+
expect(draft.isEnabled).toBe(true)
141+
142+
await disableDraftMode()
143+
expect(draft.disable).toHaveBeenCalledTimes(1)
144+
145+
// Re-assign draftMode because local variable is not updated
146+
draft = await draftMode()
147+
expect(draft.isEnabled).toBe(false)
136148
})
137149

138150
test("returns a response object", async () => {
139-
const response = disableDraftMode()
151+
const response = await disableDraftMode()
140152

141153
expect(response).toBeInstanceOf(Response)
142154
expect(response.ok).toBe(true)
@@ -156,51 +168,65 @@ describe("getDraftData()", () => {
156168
secure: true,
157169
}
158170

159-
test("returns empty object if draft mode disabled", () => {
160-
cookies().set(draftDataCookie)
171+
test("returns empty object if draft mode disabled", async () => {
172+
const cookieStore = await cookies()
173+
const draft = await draftMode()
174+
cookieStore.set(draftDataCookie)
161175

162-
const data = getDraftData()
163-
expect(draftMode().isEnabled).toBe(false)
164-
expect(cookies().has).toHaveBeenCalledTimes(0)
165-
expect(cookies().get).toHaveBeenCalledTimes(0)
176+
const data = await getDraftData()
177+
expect(draft.isEnabled).toBe(false)
178+
expect(cookieStore.has).toHaveBeenCalledTimes(0)
179+
expect(cookieStore.get).toHaveBeenCalledTimes(0)
166180
expect(data).toMatchObject({})
167181
})
168182

169-
test("returns empty object if no draft data cookie", () => {
170-
draftMode().enable()
183+
test("returns empty object if no draft data cookie", async () => {
184+
let draft = await draftMode()
185+
const cookieStore = await cookies()
186+
draft.enable()
171187
draftMode.mockClear()
172188

173-
const data = getDraftData()
189+
const data = await getDraftData()
174190
expect(draftMode).toHaveBeenCalledTimes(1)
175-
expect(draftMode().isEnabled).toBe(true)
176-
expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
177-
expect(cookies().has).toHaveBeenCalledTimes(1)
178-
expect(cookies().get).toHaveBeenCalledTimes(0)
191+
// Re-assign draftMode because local variable is not updated
192+
draft = await draftMode()
193+
expect(draft.isEnabled).toBe(true)
194+
expect(cookieStore.has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
195+
expect(cookieStore.has).toHaveBeenCalledTimes(1)
196+
expect(cookieStore.get).toHaveBeenCalledTimes(0)
179197
expect(data).toMatchObject({})
180198
})
181199

182-
test("returns empty object if no draft data cookie value", () => {
183-
cookies().set({
200+
test("returns empty object if no draft data cookie value", async () => {
201+
let draft = await draftMode()
202+
const cookieStore = await cookies()
203+
204+
cookieStore.set({
184205
...draftDataCookie,
185206
value: "",
186207
})
187-
draftMode().enable()
208+
draft.enable()
188209
draftMode.mockClear()
189210

190-
const data = getDraftData()
211+
const data = await getDraftData()
191212
expect(draftMode).toHaveBeenCalledTimes(1)
192-
expect(draftMode().isEnabled).toBe(true)
193-
expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
194-
expect(cookies().has).toHaveBeenCalledTimes(1)
195-
expect(cookies().get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
196-
expect(cookies().get).toHaveBeenCalledTimes(1)
213+
// Re-assign draftMode because local variable is not updated
214+
draft = await draftMode()
215+
expect(draft.isEnabled).toBe(true)
216+
expect(cookieStore.has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
217+
expect(cookieStore.has).toHaveBeenCalledTimes(1)
218+
expect(cookieStore.get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
219+
expect(cookieStore.get).toHaveBeenCalledTimes(1)
197220
expect(data).toMatchObject({})
198221
})
199222

200-
test("returns the JSON.parse()d data", () => {
201-
cookies().set(draftDataCookie)
202-
draftMode().enable()
223+
test("returns the JSON.parse()d data", async () => {
224+
const draft = await draftMode()
225+
const cookieStore = await cookies()
226+
227+
cookieStore.set(draftDataCookie)
228+
draft.enable()
203229

204-
expect(getDraftData()).toMatchObject(draftData)
230+
expect(await getDraftData()).toMatchObject(draftData)
205231
})
206232
})

packages/next-drupal/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"extends": "../../tsconfig.json",
3-
"include": ["src/**/*.ts"]
3+
"include": ["src/**/*.ts", "node_modules/next/types/*.d.ts"]
44
}

starters/basic-starter/app/[...slug]/page.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async function getNode(slug: string[]) {
1212

1313
const params: JsonApiParams = {}
1414

15-
const draftData = getDraftData()
15+
const draftData = await getDraftData()
1616

1717
if (draftData.path === path) {
1818
params.resourceVersion = draftData.resourceVersion
@@ -60,13 +60,17 @@ type NodePageParams = {
6060
}
6161
type NodePageProps = {
6262
params: NodePageParams
63-
searchParams: { [key: string]: string | string[] | undefined }
63+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
6464
}
6565

6666
export async function generateMetadata(
67-
{ params: { slug } }: NodePageProps,
67+
props: NodePageProps,
6868
parent: ResolvingMetadata
6969
): Promise<Metadata> {
70+
const params = await props.params
71+
72+
const { slug } = params
73+
7074
let node
7175
try {
7276
node = await getNode(slug)
@@ -108,11 +112,13 @@ export async function generateStaticParams(): Promise<NodePageParams[]> {
108112
})
109113
}
110114

111-
export default async function NodePage({
112-
params: { slug },
113-
searchParams,
114-
}: NodePageProps) {
115-
const isDraftMode = draftMode().isEnabled
115+
export default async function NodePage(props: NodePageProps) {
116+
const params = await props.params
117+
118+
const { slug } = params
119+
120+
const draft = await draftMode()
121+
const isDraftMode = draft.isEnabled
116122

117123
let node
118124
try {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { disableDraftMode } from "next-drupal/draft"
22
import type { NextRequest } from "next/server"
33

4-
export async function GET(request: NextRequest) {
5-
return disableDraftMode()
4+
export async function GET(_: NextRequest) {
5+
return await disableDraftMode()
66
}

starters/basic-starter/app/api/draft/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import { enableDraftMode } from "next-drupal/draft"
33
import type { NextRequest } from "next/server"
44

55
export async function GET(request: NextRequest): Promise<Response | never> {
6-
return enableDraftMode(request, drupal)
6+
return await enableDraftMode(request, drupal)
77
}

0 commit comments

Comments
 (0)