Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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": "^14.2.21 || ^15.1.2",
"node-cache": "^5.1.2",
"qs": "^6.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"qs": "^6.13.1",
"react": "^18.2 || ^19.0",
"react-dom": "^18.2 || ^19.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,16 @@ 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()
const draft = await draftMode()
draft.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 +43,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 +56,11 @@ 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)
const draft = await draftMode()
draft.disable()

return new Response("Draft mode is disabled")
}
Expand All @@ -66,11 +70,12 @@ 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()
const draft = await draftMode()
if (draft.isEnabled && cookieStore.has(DRAFT_DATA_COOKIE_NAME)) {
data = JSON.parse(cookieStore.get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
}

return data
Expand Down
126 changes: 76 additions & 50 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 @@ -51,6 +51,7 @@ describe("enableDraftMode()", () => {
`https://example.com/api/draft?${searchParams}`
)
const drupal = new NextDrupalBase(BASE_URL)

const draftModeCookie: ResponseCookie = {
name: DRAFT_MODE_COOKIE_NAME,
value: "some-secret-key",
Expand All @@ -62,7 +63,8 @@ describe("enableDraftMode()", () => {

const response = await enableDraftMode(request, drupal)

expect(draftMode().enable).not.toHaveBeenCalled()
const draft = await draftMode()
expect(draft.enable).not.toHaveBeenCalled()
expect(response).toBeInstanceOf(Response)
expect(response.status).toBe(500)
})
Expand All @@ -71,31 +73,35 @@ describe("enableDraftMode()", () => {
spyOnFetch({ responseBody: validationPayload })

await enableDraftMode(request, drupal)

expect(draftMode().enable).toHaveBeenCalled()
const draft = await draftMode()
expect(draft.enable).toHaveBeenCalled()
})

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

const cookieStore = await cookies()
// 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()
cookieStore.set(draftModeCookie)

expect(cookieStore.get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax")
expect(cookieStore.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(cookieStore.get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none")
expect(cookieStore.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)
const cookieStore = await cookies()

expect(cookieStore.get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined)

await enableDraftMode(request, drupal)

const cookie = cookies().get(DRAFT_DATA_COOKIE_NAME)
const cookie = cookieStore.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 +124,31 @@ describe("enableDraftMode()", () => {
})

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

expect(cookies).toHaveBeenCalledTimes(1)
expect(cookies().delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
test("draft data cookie was deleted", async () => {
const cookieStore = await cookies()
await disableDraftMode()
expect(cookies).toHaveBeenCalledTimes(2)
expect(cookieStore.delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
})

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

disableDraftMode()
expect(draftMode().disable).toHaveBeenCalledTimes(1)
expect(draftMode().isEnabled).toBe(false)
// Re-assign draftMode because local variable is not updated
draft = await draftMode()
expect(draft.isEnabled).toBe(true)

await disableDraftMode()
expect(draft.disable).toHaveBeenCalledTimes(1)

// Re-assign draftMode because local variable is not updated
draft = await draftMode()
expect(draft.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 +168,65 @@ describe("getDraftData()", () => {
secure: true,
}

test("returns empty object if draft mode disabled", () => {
cookies().set(draftDataCookie)
test("returns empty object if draft mode disabled", async () => {
const cookieStore = await cookies()
const draft = await draftMode()
cookieStore.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(draft.isEnabled).toBe(false)
expect(cookieStore.has).toHaveBeenCalledTimes(0)
expect(cookieStore.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 () => {
let draft = await draftMode()
const cookieStore = await cookies()
draft.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)
// Re-assign draftMode because local variable is not updated
draft = await draftMode()
expect(draft.isEnabled).toBe(true)
expect(cookieStore.has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookieStore.has).toHaveBeenCalledTimes(1)
expect(cookieStore.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 () => {
let draft = await draftMode()
const cookieStore = await cookies()

cookieStore.set({
...draftDataCookie,
value: "",
})
draftMode().enable()
draft.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)
// Re-assign draftMode because local variable is not updated
draft = await draftMode()
expect(draft.isEnabled).toBe(true)
expect(cookieStore.has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookieStore.has).toHaveBeenCalledTimes(1)
expect(cookieStore.get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME)
expect(cookieStore.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 () => {
const draft = await draftMode()
const cookieStore = await cookies()

cookieStore.set(draftDataCookie)
draft.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"]
}
22 changes: 14 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,13 @@ 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 draft = await draftMode()
const isDraftMode = draft.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)
}
Loading
Loading