Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Full migration from app router PLUS next15/react19 #450

Merged
merged 15 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/sour-beds-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"socialify": minor
---

Full migration from page router to app router.

Upgraded to nextjs15/react19 via official codemod and applied type fixes.
63 changes: 63 additions & 0 deletions .playwright/imageAPIEndpoints.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type Page, expect, test } from '@playwright/test'

const customTimeout = { timeout: 30000 }

const defaultImageURL: string =
'/wei/socialify/image?description=1&font=Raleway&language=1&name=1&owner=1&pattern=Diagonal%20Stripes&theme=Dark'
const svgImageURL: string =
'/wei/socialify/svg?description=1&font=Raleway&language=1&name=1&owner=1&pattern=Diagonal%20Stripes&theme=Dark'
const pngImageURL: string =
'/wei/socialify/png?description=1&font=Raleway&language=1&name=1&owner=1&pattern=Diagonal%20Stripes&theme=Dark'
// Backward compatibility route.
const jpgImageURL: string =
'/wei/socialify/jpg?description=1&font=Raleway&language=1&name=1&owner=1&pattern=Diagonal%20Stripes&theme=Dark'

test.describe('Socialify image api', () => {
test('respond consistently for default endpoint', async ({
page,
}: { page: Page }): Promise<void> => {
await page.goto(defaultImageURL, customTimeout)

// Wait for the page to load/hydrate completely.
await page.waitForLoadState('networkidle', customTimeout)

const image = await page.screenshot()
expect(image).toMatchSnapshot()
})

test('respond consistently for svg endpoint', async ({
page,
}: { page: Page }): Promise<void> => {
await page.goto(svgImageURL, customTimeout)

// Wait for the page to load/hydrate completely.
await page.waitForLoadState('networkidle', customTimeout)

const image = await page.screenshot()
expect(image).toMatchSnapshot()
})

test('respond consistently for png endpoint', async ({
page,
}: { page: Page }): Promise<void> => {
await page.goto(pngImageURL, customTimeout)

// Wait for the page to load/hydrate completely.
await page.waitForLoadState('networkidle', customTimeout)

const image = await page.screenshot()
expect(image).toMatchSnapshot()
})

test('respond consistently for backwards-compatible jpg endpoint', async ({
page,
}: { page: Page }): Promise<void> => {
await page.goto(jpgImageURL, customTimeout)

// Wait for the page to load/hydrate completely.
await page.waitForLoadState('networkidle', customTimeout)

const image = await page.screenshot()
expect(image).toMatchSnapshot()
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion .playwright/simpleUserStory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type Page, expect, test } from '@playwright/test'

const customTimeout = { timeout: 30000 }
const componentUpdateTimeout = 1000

// Testing constants.
const repo: string = 'wei/socialify'
const expectedConfigURL: string =
'/wei/socialify?language=1&owner=1&name=1&stargazers=1&theme=Light'
const expectedImageURLRegExp: RegExp =
/\/wei\/socialify\/image\?description=1&language=1&name=1&owner=1&theme=Light$/
/\/wei\/socialify\/image\?description=1&font=Source\+Code\+Pro&language=1&name=1&owner=1&theme=Light$/

async function getClipboardText(page: Page): Promise<string> {
return await page.evaluate(async () => {
Expand All @@ -28,6 +29,7 @@ test.describe('A simple user story:', () => {
}: { page: Page }): Promise<void> => {
// Input and submit the repo following accessibility best practices.
await page.fill('input[name="repo-input"]', repo)
await page.waitForTimeout(componentUpdateTimeout)
await page.click('button[type="submit"]')

// Wait for navigation to the preview config page.
Expand All @@ -38,7 +40,12 @@ test.describe('A simple user story:', () => {
await expect(page).toHaveURL(expectedConfigURL)

await page.click('input[name="stargazers"]')
await page.waitForTimeout(componentUpdateTimeout)
await page.click('input[name="description"]')
await page.waitForTimeout(componentUpdateTimeout)
// Select the "Source Code Pro" option for max diff from default.
await page.selectOption('select[name="font"]', { label: 'Source Code Pro' })
await page.waitForTimeout(componentUpdateTimeout)

// Obtain the consistent preview image URL.
await page.click('button:has-text("URL")')
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Wei He
Copyright (c) 2024 Wei He <https://wei.mit-license.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
9 changes: 9 additions & 0 deletions app/[_owner]/[_name]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use client'

import { JSX } from 'react'

import MainRenderer from '@/src/components/mainRenderer'

export default function PreviewConfigPage(): JSX.Element {
return <MainRenderer />
}
7 changes: 3 additions & 4 deletions pages/api/font.ts → app/api/font/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import type { NextRequest } from 'next/server'

import { FontDetector, languageFontMap } from '@/common/font'

export const config = {
runtime: 'edge',
}
export const runtime = 'edge'

const detector = new FontDetector()

Expand Down Expand Up @@ -37,7 +35,8 @@ function encodeFontInfoAsArrayBuffer(code: string, fontData: ArrayBuffer) {
return buffer
}

export default async function loadGoogleFont(req: NextRequest) {
// export default async function loadGoogleFont(req: NextRequest) {
export async function GET(req: NextRequest) {
if (req.nextUrl.pathname !== '/api/font') return

const { searchParams } = new URL(req.url)
Expand Down
24 changes: 7 additions & 17 deletions pages/api/graphql.ts → app/api/graphql/route.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import 'server-only'
import type { NextRequest } from 'next/server'

const API_ENDPOINT: string = 'https://api.github.com/graphql'
import { GITHUB_GRAPHQL_ENDPOINT } from '@/common/constants'

const graphQLEndpoint = async (req: NextRequest) => {
if (req.method !== 'POST') {
return new Response('Method Not Allowed', {
status: 405,
headers: {
'cache-control': 'max-age=0, public',
},
})
}
export const runtime = 'edge'

const response = await fetch(API_ENDPOINT, {
export async function POST(req: NextRequest) {
const response = await fetch(GITHUB_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
'content-type': 'application/json',
},
body: req.body,
// @ts-expect-error: 'duplex' is not part of the RequestInit type but required by GraphQL.
duplex: 'half',
KemingHe marked this conversation as resolved.
Show resolved Hide resolved
})

if (!response.ok) {
Expand All @@ -41,9 +37,3 @@ const graphQLEndpoint = async (req: NextRequest) => {
},
})
}

export const config = {
runtime: 'edge',
}

export default graphQLEndpoint
15 changes: 15 additions & 0 deletions app/api/image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { isBot } from 'next/dist/server/web/spec-extension/user-agent'
import type { NextRequest, NextResponse } from 'next/server'

import { GET as GETPng } from '@/app/api/png/route'
import { GET as GETSvg } from '@/app/api/svg/route'

export const runtime = 'edge'

export async function GET(req: NextRequest): Promise<NextResponse> {
if (isBot(req.headers.get('user-agent') ?? '')) {
return GETPng(req)
} else {
return GETSvg(req)
}
}
16 changes: 6 additions & 10 deletions pages/api/png.ts → app/api/png/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { NextRequest } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'

import renderCardPNG from '@/common/renderPNG'
import type QueryType from '@/common/types/queryType'

const pngEndpoint = async (req: NextRequest) => {
export const runtime = 'edge'

export async function GET(req: NextRequest): Promise<NextResponse> {
const { searchParams } = new URL(req.url)
const query = Object.fromEntries(searchParams) as QueryType

try {
return new Response(await renderCardPNG(query), {
return new NextResponse(await renderCardPNG(query), {
status: 200,
headers: {
'content-type': 'image/png',
Expand All @@ -27,7 +29,7 @@ const pngEndpoint = async (req: NextRequest) => {
}
console.error(errorJSON)

return new Response(JSON.stringify(errorJSON), {
return new NextResponse(JSON.stringify(errorJSON), {
status: 400,
headers: {
'content-type': 'application/json',
Expand All @@ -36,9 +38,3 @@ const pngEndpoint = async (req: NextRequest) => {
})
}
}

export const config = {
runtime: 'edge',
}

export default pngEndpoint
18 changes: 7 additions & 11 deletions pages/api/stats.svg.ts → app/api/stats.svg/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { badgen } from 'badgen'
import type { NextRequest } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'

import statsEndpoint from '@/pages/api/stats'
import { GET as GETStats } from '@/app/api/stats/route'

const statsSvgEndpoint = async (req: NextRequest) => {
export const runtime = 'edge'

export async function GET(req: NextRequest): Promise<NextResponse> {
let totalCount = 0

try {
const apiResponse = await (await statsEndpoint(req)).json()
const apiResponse = await (await GETStats(req)).json()
if (apiResponse.total_count) {
totalCount = apiResponse.total_count
}
Expand All @@ -33,7 +35,7 @@ const statsSvgEndpoint = async (req: NextRequest) => {
style: 'flat',
})

return new Response(svg, {
return new NextResponse(svg, {
status: 200,
headers: {
'content-type': 'image/svg+xml',
Expand All @@ -43,9 +45,3 @@ const statsSvgEndpoint = async (req: NextRequest) => {
},
})
}

export const config = {
runtime: 'edge',
}

export default statsSvgEndpoint
24 changes: 11 additions & 13 deletions pages/api/stats.ts → app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import type { NextRequest } from 'next/server'
import 'server-only'
import { GITHUB_API_ENDPOINT } from '@/common/constants'
import { type NextRequest, NextResponse } from 'next/server'

const statsEndpoint = async (_req: NextRequest) => {
export const runtime = 'edge'

export async function GET(_req: NextRequest): Promise<NextResponse> {
const response = await fetch(
`https://api.github.com/search/code?per_page=1&q=${encodeURIComponent(
`${GITHUB_API_ENDPOINT}/search/code?per_page=1&q=${encodeURIComponent(
'socialify.git.ci'
)}`,
{
method: 'GET',
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
accept: 'application/vnd.github.v3+json',
authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'content-type': 'application/json',
},
}
)

if (!response.ok) {
return new Response(await response.text(), {
return new NextResponse(await response.text(), {
status: response.status,
headers: {
'cache-control': 'public, max-age=0',
Expand All @@ -25,7 +29,7 @@ const statsEndpoint = async (_req: NextRequest) => {
}

const json = await response.json()
return new Response(JSON.stringify({ total_count: json.total_count }), {
return new NextResponse(JSON.stringify({ total_count: json.total_count }), {
status: 200,
headers: {
'content-type': 'application/json',
Expand All @@ -35,9 +39,3 @@ const statsEndpoint = async (_req: NextRequest) => {
},
})
}

export const config = {
runtime: 'edge',
}

export default statsEndpoint
16 changes: 6 additions & 10 deletions pages/api/svg.ts → app/api/svg/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type { NextRequest } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'

import renderCardSVG from '@/common/renderSVG'
import type QueryType from '@/common/types/queryType'

const svgEndpoint = async (req: NextRequest) => {
export const runtime = 'edge'

export async function GET(req: NextRequest): Promise<NextResponse> {
const { searchParams } = new URL(req.url)
const query = Object.fromEntries(searchParams) as QueryType

try {
const svg = await renderCardSVG(query)

return new Response(svg, {
return new NextResponse(svg, {
status: 200,
headers: {
'content-type': 'image/svg+xml',
Expand All @@ -29,7 +31,7 @@ const svgEndpoint = async (req: NextRequest) => {
}
console.error(errorJSON)

return new Response(JSON.stringify(errorJSON), {
return new NextResponse(JSON.stringify(errorJSON), {
status: 400,
headers: {
'content-type': 'application/json',
Expand All @@ -38,9 +40,3 @@ const svgEndpoint = async (req: NextRequest) => {
})
}
}

export const config = {
runtime: 'edge',
}

export default svgEndpoint
File renamed without changes
File renamed without changes.
Loading
Loading