diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx index 0ac4fbe72fa..0129527cc2b 100644 --- a/packages/next/src/views/Root/index.tsx +++ b/packages/next/src/views/Root/index.tsx @@ -63,10 +63,14 @@ export const RootPage = async ({ const params = await paramsPromise - const currentRoute = formatAdminURL({ + const rawCurrentRoute = formatAdminURL({ adminRoute, path: Array.isArray(params.segments) ? `/${params.segments.join('/')}` : null, }) + const currentRoute = + rawCurrentRoute.length > 1 && rawCurrentRoute.endsWith('/') + ? rawCurrentRoute.slice(0, -1) + : rawCurrentRoute const segments = Array.isArray(params.segments) ? params.segments : [] const isCollectionRoute = segments[0] === 'collections' @@ -223,10 +227,14 @@ export const RootPage = async ({ const usersCollection = config.collections.find(({ slug }) => slug === userSlug) const disableLocalStrategy = usersCollection?.auth?.disableLocalStrategy - const createFirstUserRoute = formatAdminURL({ + const rawCreateFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute, }) + const createFirstUserRoute = + rawCreateFirstUserRoute.length > 1 && rawCreateFirstUserRoute.endsWith('/') + ? rawCreateFirstUserRoute.slice(0, -1) + : rawCreateFirstUserRoute if (disableLocalStrategy && currentRoute === createFirstUserRoute) { redirect(adminRoute) diff --git a/packages/next/src/withPayload/withPayload.js b/packages/next/src/withPayload/withPayload.js index 465251cfd8d..08a5ebdc0fc 100644 --- a/packages/next/src/withPayload/withPayload.js +++ b/packages/next/src/withPayload/withPayload.js @@ -270,6 +270,11 @@ export const withPayload = (nextConfig = {}, options = {}) => { baseConfig.env.NEXT_BASE_PATH = nextConfig.basePath } + if (nextConfig.trailingSlash === true) { + process.env.NEXT_TRAILING_SLASH = 'true' + baseConfig.env.NEXT_TRAILING_SLASH = 'true' + } + if (!supportsTurbopackBuild) { return withPayloadLegacy(baseConfig) } else { diff --git a/packages/payload/src/utilities/formatAdminURL.spec.ts b/packages/payload/src/utilities/formatAdminURL.spec.ts index 0674e7f1fed..2c73f6b9076 100644 --- a/packages/payload/src/utilities/formatAdminURL.spec.ts +++ b/packages/payload/src/utilities/formatAdminURL.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { afterEach, beforeEach, describe, it, expect } from 'vitest' import { formatAdminURL } from './formatAdminURL.js' describe('formatAdminURL', () => { @@ -214,4 +214,119 @@ describe('formatAdminURL', () => { expect(result).toBe('/') }) }) + + describe('trailing slash handling', () => { + const originalTrailingSlash = process.env.NEXT_TRAILING_SLASH + + beforeEach(() => { + process.env.NEXT_TRAILING_SLASH = 'true' + }) + + afterEach(() => { + if (originalTrailingSlash === undefined) { + delete process.env.NEXT_TRAILING_SLASH + } else { + process.env.NEXT_TRAILING_SLASH = originalTrailingSlash + } + }) + + it('should append trailing slash to relative admin URL', () => { + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path: dummyPath, + relative: true, + }) + + expect(result).toBe(`${defaultAdminRoute}${dummyPath}/`) + }) + + it('should append trailing slash to relative api URL', () => { + const result = formatAdminURL({ + apiRoute: '/api', + path: '/users', + relative: true, + }) + + expect(result).toBe(`${process.env.NEXT_BASE_PATH || ''}/api/users/`) + }) + + it('should append trailing slash to absolute URL', () => { + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path: dummyPath, + serverURL, + }) + + expect(result).toBe( + `${serverURL}${process.env.NEXT_BASE_PATH || ''}${defaultAdminRoute}${dummyPath}/`, + ) + }) + + it('should append trailing slash when basePath is set', () => { + const result = formatAdminURL({ + apiRoute: '/api', + basePath: '/v1', + path: '/users', + serverURL, + }) + + expect(result).toBe(`${serverURL}${process.env.NEXT_BASE_PATH || ''}/v1/api/users/`) + }) + + it('should not append trailing slash to root "/"', () => { + const result = formatAdminURL({ + adminRoute: rootAdminRoute, + relative: true, + }) + + expect(result).toBe('/') + }) + + it('should not double-slash when path already ends with /', () => { + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path: '/collections/posts', + relative: true, + }) + + expect(result.endsWith('//')).toBe(false) + expect(result).toBe(`${defaultAdminRoute}/collections/posts/`) + }) + + it('should place trailing slash before query string', () => { + const path = `${dummyPath}?page=2` + + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path, + relative: true, + }) + + expect(result).toBe(`${defaultAdminRoute}${dummyPath}/?page=2`) + }) + + it('should leave URLs unchanged when env var is not set', () => { + delete process.env.NEXT_TRAILING_SLASH + + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path: dummyPath, + relative: true, + }) + + expect(result).toBe(`${defaultAdminRoute}${dummyPath}`) + }) + + it('should leave URLs unchanged when env var is "false"', () => { + process.env.NEXT_TRAILING_SLASH = 'false' + + const result = formatAdminURL({ + adminRoute: defaultAdminRoute, + path: dummyPath, + relative: true, + }) + + expect(result).toBe(`${defaultAdminRoute}${dummyPath}`) + }) + }) }) diff --git a/packages/payload/src/utilities/formatAdminURL.ts b/packages/payload/src/utilities/formatAdminURL.ts index ec42581fcb1..fee0c8cd041 100644 --- a/packages/payload/src/utilities/formatAdminURL.ts +++ b/packages/payload/src/utilities/formatAdminURL.ts @@ -51,11 +51,24 @@ export const formatAdminURL = (args: FormatURLArgs): string => { if (relative || !serverURL) { if (includeBasePath && basePath) { - return pathnameWithBase + return applyTrailingSlash(pathnameWithBase) } - return pathname + return applyTrailingSlash(pathname) } const serverURLObj = new URL(serverURL) - return new URL(pathnameWithBase, serverURLObj.origin).toString() + return applyTrailingSlash(new URL(pathnameWithBase, serverURLObj.origin).toString()) +} + +const applyTrailingSlash = (url: string): string => { + if (process.env.NEXT_TRAILING_SLASH !== 'true') { + return url + } + const queryIndex = url.search(/[?#]/) + const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex) + const queryPart = queryIndex === -1 ? '' : url.slice(queryIndex) + if (pathPart.endsWith('/')) { + return url + } + return `${pathPart}/${queryPart}` } diff --git a/packages/payload/src/utilities/handleEndpoints.ts b/packages/payload/src/utilities/handleEndpoints.ts index b69f10d5f53..eb9b8d0a470 100644 --- a/packages/payload/src/utilities/handleEndpoints.ts +++ b/packages/payload/src/utilities/handleEndpoints.ts @@ -143,11 +143,14 @@ export const handleEndpoints = async ({ const { payload } = req const { config } = payload - const pathname = path ?? new URL(req.url!).pathname - const baseAPIPath = formatAdminURL({ + const rawPathname = path ?? new URL(req.url!).pathname + const pathname = rawPathname.length > 1 ? rawPathname.replace(/\/$/, '') : rawPathname + const rawBaseAPIPath = formatAdminURL({ apiRoute: config.routes.api, path: '', }) + const baseAPIPath = + rawBaseAPIPath.length > 1 ? rawBaseAPIPath.replace(/\/$/, '') : rawBaseAPIPath if (!pathname.startsWith(baseAPIPath)) { return notFoundResponse(req, pathname) diff --git a/test/trailing-slash/.gitignore b/test/trailing-slash/.gitignore new file mode 100644 index 00000000000..cce01755f4f --- /dev/null +++ b/test/trailing-slash/.gitignore @@ -0,0 +1,2 @@ +/media +/media-gif diff --git a/test/trailing-slash/app/(payload)/admin/[[...segments]]/not-found.tsx b/test/trailing-slash/app/(payload)/admin/[[...segments]]/not-found.tsx new file mode 100644 index 00000000000..180e6f81cdf --- /dev/null +++ b/test/trailing-slash/app/(payload)/admin/[[...segments]]/not-found.tsx @@ -0,0 +1,25 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from 'next' + +import config from '@payload-config' +import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' + +import { importMap } from '../importMap.js' + +type Args = { + params: Promise<{ + segments: string[] + }> + searchParams: Promise<{ + [key: string]: string | string[] + }> +} + +export const generateMetadata = ({ params, searchParams }: Args): Promise => + generatePageMetadata({ config, params, searchParams }) + +const NotFound = ({ params, searchParams }: Args) => + NotFoundPage({ config, importMap, params, searchParams }) + +export default NotFound diff --git a/test/trailing-slash/app/(payload)/admin/[[...segments]]/page.tsx b/test/trailing-slash/app/(payload)/admin/[[...segments]]/page.tsx new file mode 100644 index 00000000000..e59b2d3a84b --- /dev/null +++ b/test/trailing-slash/app/(payload)/admin/[[...segments]]/page.tsx @@ -0,0 +1,25 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from 'next' + +import config from '@payload-config' +import { generatePageMetadata, RootPage } from '@payloadcms/next/views' + +import { importMap } from '../importMap.js' + +type Args = { + params: Promise<{ + segments: string[] + }> + searchParams: Promise<{ + [key: string]: string | string[] + }> +} + +export const generateMetadata = ({ params, searchParams }: Args): Promise => + generatePageMetadata({ config, params, searchParams }) + +const Page = ({ params, searchParams }: Args) => + RootPage({ config, importMap, params, searchParams }) + +export default Page diff --git a/test/trailing-slash/app/(payload)/admin/importMap.js b/test/trailing-slash/app/(payload)/admin/importMap.js new file mode 100644 index 00000000000..7abb62dd90e --- /dev/null +++ b/test/trailing-slash/app/(payload)/admin/importMap.js @@ -0,0 +1,6 @@ +import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' + +/** @type import('payload').ImportMap */ +export const importMap = { + '@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1, +} diff --git a/test/trailing-slash/app/(payload)/api/[...slug]/route.ts b/test/trailing-slash/app/(payload)/api/[...slug]/route.ts new file mode 100644 index 00000000000..183cf457f62 --- /dev/null +++ b/test/trailing-slash/app/(payload)/api/[...slug]/route.ts @@ -0,0 +1,10 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from '@payload-config' +import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' + +export const GET = REST_GET(config) +export const POST = REST_POST(config) +export const DELETE = REST_DELETE(config) +export const PATCH = REST_PATCH(config) +export const OPTIONS = REST_OPTIONS(config) diff --git a/test/trailing-slash/app/(payload)/api/graphql-playground/route.ts b/test/trailing-slash/app/(payload)/api/graphql-playground/route.ts new file mode 100644 index 00000000000..dffacb345f0 --- /dev/null +++ b/test/trailing-slash/app/(payload)/api/graphql-playground/route.ts @@ -0,0 +1,6 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from '@payload-config' +import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js' + +export const GET = GRAPHQL_PLAYGROUND_GET(config) diff --git a/test/trailing-slash/app/(payload)/api/graphql/route.ts b/test/trailing-slash/app/(payload)/api/graphql/route.ts new file mode 100644 index 00000000000..2069ff86b0a --- /dev/null +++ b/test/trailing-slash/app/(payload)/api/graphql/route.ts @@ -0,0 +1,8 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from '@payload-config' +import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' + +export const POST = GRAPHQL_POST(config) + +export const OPTIONS = REST_OPTIONS(config) diff --git a/test/trailing-slash/app/(payload)/custom.scss b/test/trailing-slash/app/(payload)/custom.scss new file mode 100644 index 00000000000..f557cd4277e --- /dev/null +++ b/test/trailing-slash/app/(payload)/custom.scss @@ -0,0 +1,7 @@ +#custom-css { + font-family: monospace; +} + +#custom-css::after { + content: 'custom-css'; +} diff --git a/test/trailing-slash/app/(payload)/layout.tsx b/test/trailing-slash/app/(payload)/layout.tsx new file mode 100644 index 00000000000..93a5fcf45cf --- /dev/null +++ b/test/trailing-slash/app/(payload)/layout.tsx @@ -0,0 +1,31 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { ServerFunctionClient } from 'payload' + +import config from '@payload-config' +import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' +import React from 'react' + +import { importMap } from './admin/importMap.js' +import './custom.scss' + +type Args = { + children: React.ReactNode +} + +const serverFunction: ServerFunctionClient = async function (args) { + 'use server' + return handleServerFunctions({ + ...args, + config, + importMap, + }) +} + +const Layout = ({ children }: Args) => ( + + {children} + +) + +export default Layout diff --git a/test/trailing-slash/collections/Posts.ts b/test/trailing-slash/collections/Posts.ts new file mode 100644 index 00000000000..e3ff13f4572 --- /dev/null +++ b/test/trailing-slash/collections/Posts.ts @@ -0,0 +1,19 @@ +import type { CollectionConfig } from 'payload' + +export const Posts: CollectionConfig = { + slug: 'posts', + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'content', + type: 'textarea', + }, + ], +} diff --git a/test/trailing-slash/config.ts b/test/trailing-slash/config.ts new file mode 100644 index 00000000000..139eb88f528 --- /dev/null +++ b/test/trailing-slash/config.ts @@ -0,0 +1,14 @@ +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { Posts } from './collections/Posts.js' +import { seed } from './seed/index.js' + +process.env.NEXT_TRAILING_SLASH = 'true' + +export default buildConfigWithDefaults({ + admin: { + autoLogin: false, + }, + collections: [Posts], + onInit: seed, + serverURL: `http://localhost:${process.env.PORT || 3000}`, +}) diff --git a/test/trailing-slash/e2e.spec.ts b/test/trailing-slash/e2e.spec.ts new file mode 100644 index 00000000000..7a29427788e --- /dev/null +++ b/test/trailing-slash/e2e.spec.ts @@ -0,0 +1,163 @@ +import type { Page, Request, Response } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import { login } from '../__helpers/e2e/auth/login.js' +import { goToListDoc } from '../__helpers/e2e/goToListDoc.js' +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, +} from '../__helpers/e2e/helpers.js' +import { AdminUrlUtil } from '../__helpers/shared/adminUrlUtil.js' +import { initPayloadE2ENoConfig } from '../__helpers/shared/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../playwright.config.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +process.env.NEXT_TRAILING_SLASH = 'true' + +test.describe('Trailing Slash', () => { + let page: Page + let url: AdminUrlUtil + let serverURL: string + const apiResponses: { method: string; status: number; url: string }[] = [] + const recordResponse = (response: Response) => { + const request = response.request() + if (response.url().includes('/api/')) { + apiResponses.push({ + method: request.method(), + status: response.status(), + url: response.url(), + }) + } + } + const recordRequest = (request: Request) => { + if (request.url().includes('/api/')) { + apiResponses.push({ + method: request.method(), + status: 0, + url: request.url(), + }) + } + } + + test.beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + + const { payload } = await initPayloadE2ENoConfig({ + dirname, + }) + serverURL = payload.serverURL + url = new AdminUrlUtil(serverURL, 'posts') + + const context = await browser.newContext() + page = await context.newPage() + initPageConsoleErrorCatch(page) + page.on('response', recordResponse) + + await ensureCompilationIsDone({ + noAutoLogin: true, + page, + readyURL: `${serverURL}/admin/login/**`, + serverURL, + }) + }) + + test.beforeEach(() => { + apiResponses.length = 0 + }) + + test('should render forgot-password form action with trailing slash', async () => { + await page.goto(`${url.admin}/forgot`) + + await expect(async () => { + const formAction = await page.locator('form').getAttribute('action') + expect(formAction).toContain('/api/users/forgot-password/') + }).toPass() + }) + + test('should login without 308 redirect on /api/users/login', async () => { + await login({ page, serverURL }) + + await expect(() => { + const loginRequest = apiResponses.find( + (entry) => entry.method === 'POST' && entry.url.includes('/api/users/login'), + ) + expect(loginRequest?.url).toMatch(/\/api\/users\/login\/(\?|$)/) + expect(loginRequest?.status).toBeLessThan(300) + }).toPass() + }) + + test('should navigate dashboard, list, edit and account without any /api 3xx', async () => { + apiResponses.length = 0 + + await page.goto(url.admin) + await expect(page.locator('.dashboard')).toBeVisible() + + await page.goto(url.list) + await expect(page.locator('.collection-list')).toBeVisible() + + await goToListDoc({ + cellClass: '.cell-title', + page, + textToMatch: 'First Post', + urlUtil: url, + }) + await expect(page.locator('#field-title')).toBeVisible() + + await page.goto(`${url.admin}/account`) + await expect(page.locator('#field-email')).toBeVisible() + + await expect(() => { + const redirected = apiResponses.filter((entry) => entry.status >= 300 && entry.status < 400) + expect(redirected).toEqual([]) + }).toPass() + }) + + test('should save edits to existing doc with trailing-slash URL', async () => { + apiResponses.length = 0 + page.on('request', recordRequest) + + await goToListDoc({ + cellClass: '.cell-title', + page, + textToMatch: 'First Post', + urlUtil: url, + }) + + await page.locator('#field-title').fill('First Post Edited') + await saveDocAndAssert(page) + + page.off('request', recordRequest) + + await expect(() => { + const patchEntry = apiResponses.find( + (entry) => entry.method === 'PATCH' && entry.url.includes('/api/posts/'), + ) + expect(patchEntry?.url).toMatch(/\/api\/posts\/[^/?#]+\/(\?|$)/) + expect(patchEntry?.status).toBeLessThan(300) + }).toPass() + }) + + test('should create new doc and POST to trailing-slash URL', async () => { + apiResponses.length = 0 + + await page.goto(`${url.list}/create`) + await page.locator('#field-title').fill('Created Under Trailing Slash') + await saveDocAndAssert(page) + + await expect(page.locator('#field-title')).toHaveValue('Created Under Trailing Slash') + + await expect(() => { + const createEntry = apiResponses.find( + (entry) => entry.method === 'POST' && entry.url.includes('/api/posts/'), + ) + expect(createEntry?.url).toMatch(/\/api\/posts\/(\?|$)/) + expect(createEntry?.status).toBeLessThan(300) + }).toPass() + }) +}) diff --git a/test/trailing-slash/next.config.mjs b/test/trailing-slash/next.config.mjs new file mode 100644 index 00000000000..ebfb5ada653 --- /dev/null +++ b/test/trailing-slash/next.config.mjs @@ -0,0 +1,20 @@ +import nextConfig from '../../next.config.mjs' + +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(__filename) + +process.env.NEXT_TRAILING_SLASH = 'true' + +export default { + ...nextConfig, + trailingSlash: true, + env: { + ...nextConfig.env, + PAYLOAD_CORE_DEV: 'true', + ROOT_DIR: path.resolve(dirname), + NEXT_TRAILING_SLASH: 'true', + }, +} diff --git a/test/trailing-slash/seed/index.ts b/test/trailing-slash/seed/index.ts new file mode 100644 index 00000000000..63b6aa9ea55 --- /dev/null +++ b/test/trailing-slash/seed/index.ts @@ -0,0 +1,21 @@ +import type { Config } from 'payload' + +import { devUser } from '../../credentials.js' + +export const seed: Config['onInit'] = async (payload) => { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + + await payload.create({ + collection: 'posts', + data: { + content: 'This is the content of the first post.', + title: 'First Post', + }, + }) +} diff --git a/test/trailing-slash/shared.ts b/test/trailing-slash/shared.ts new file mode 100644 index 00000000000..b93a7863bac --- /dev/null +++ b/test/trailing-slash/shared.ts @@ -0,0 +1 @@ +export const TRAILING_SLASH = true diff --git a/test/trailing-slash/tsconfig.eslint.json b/test/trailing-slash/tsconfig.eslint.json new file mode 100644 index 00000000000..b31482bef8b --- /dev/null +++ b/test/trailing-slash/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +} diff --git a/test/trailing-slash/tsconfig.json b/test/trailing-slash/tsconfig.json new file mode 100644 index 00000000000..88a05102f38 --- /dev/null +++ b/test/trailing-slash/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@payload-config": ["./config.ts"], + "@payloadcms/ui/assets": ["../../packages/ui/src/assets/index.ts"], + "@payloadcms/ui/elements/*": ["../../packages/ui/src/elements/*/index.tsx"], + "@payloadcms/ui/fields/*": ["../../packages/ui/src/fields/*/index.tsx"], + "@payloadcms/ui/forms/*": ["../../packages/ui/src/forms/*/index.tsx"], + "@payloadcms/ui/graphics/*": ["../../packages/ui/src/graphics/*/index.tsx"], + "@payloadcms/ui/hooks/*": ["../../packages/ui/src/hooks/*.ts"], + "@payloadcms/ui/icons/*": ["../../packages/ui/src/icons/*/index.tsx"], + "@payloadcms/ui/providers/*": ["../../packages/ui/src/providers/*/index.tsx"], + "@payloadcms/ui/templates/*": ["../../packages/ui/src/templates/*/index.tsx"], + "@payloadcms/ui/utilities/*": ["../../packages/ui/src/utilities/*.ts"], + "@payloadcms/ui/scss": ["../../packages/ui/src/scss.scss"], + "@payloadcms/ui/scss/app.scss": ["../../packages/ui/src/scss/app.scss"], + "payload/types": ["../../packages/payload/src/exports/types.ts"], + "@payloadcms/next/*": ["../../packages/next/src/*"], + "@payloadcms/next": ["../../packages/next/src/exports/*"] + } + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}