From 085c99cd28e4d03a5e8391a9151f59d1b0cd76f1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Oct 2025 14:23:41 +0300 Subject: [PATCH 1/8] fix: implement localized routes penality --- .../src/client/routing/parameterization.ts | 27 +++++++++++++++++++ .../config/manifest/createRouteManifest.ts | 17 +++++++++--- packages/nextjs/src/config/manifest/types.ts | 5 ++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/client/routing/parameterization.ts b/packages/nextjs/src/client/routing/parameterization.ts index c20d71614234..32f50c5bbde6 100644 --- a/packages/nextjs/src/client/routing/parameterization.ts +++ b/packages/nextjs/src/client/routing/parameterization.ts @@ -37,6 +37,16 @@ function getRouteSpecificity(routePath: string): number { // Static segments add 0 to score as they are most specific } + if (segments.length > 0) { + // Add a small penalty based on inverse of segment count + // This ensures that routes with more segments are preferred + // e.g., '/:locale/foo' is more specific than '/:locale' + // We use a small value (1 / segments.length) so it doesn't override the main scoring + // but breaks ties between routes with the same number of dynamic segments + const segmentCountPenalty = 1 / segments.length; + score += segmentCountPenalty; + } + return score; } @@ -134,6 +144,23 @@ function findMatchingRoutes( } } + // Try matching with optional prefix segments (for i18n routing patterns) + // This handles cases like '/foo' matching '/:locale/foo' when using next-intl with localePrefix: "as-needed" + // We do this regardless of whether we found direct matches, as we want the most specific match + if (!route.startsWith('/:')) { + for (const dynamicRoute of dynamicRoutes) { + if (dynamicRoute.hasOptionalPrefix && dynamicRoute.regex) { + // Prepend a placeholder segment to simulate the optional prefix + // e.g., '/foo' becomes '/PLACEHOLDER/foo' to match '/:locale/foo' + const routeWithPrefix = `/SENTRY_OPTIONAL_PREFIX${route}`; + const regex = getCompiledRegex(dynamicRoute.regex); + if (regex?.test(routeWithPrefix)) { + matches.push(dynamicRoute.path); + } + } + } + } + return matches; } diff --git a/packages/nextjs/src/config/manifest/createRouteManifest.ts b/packages/nextjs/src/config/manifest/createRouteManifest.ts index 32e7db61b57b..8203b855cab5 100644 --- a/packages/nextjs/src/config/manifest/createRouteManifest.ts +++ b/packages/nextjs/src/config/manifest/createRouteManifest.ts @@ -47,7 +47,11 @@ function getDynamicRouteSegment(name: string): string { return `:${name.slice(1, -1)}`; } -function buildRegexForDynamicRoute(routePath: string): { regex: string; paramNames: string[] } { +function buildRegexForDynamicRoute(routePath: string): { + regex: string; + paramNames: string[]; + hasOptionalPrefix: boolean; +} { const segments = routePath.split('/').filter(Boolean); const regexSegments: string[] = []; const paramNames: string[] = []; @@ -95,7 +99,13 @@ function buildRegexForDynamicRoute(routePath: string): { regex: string; paramNam pattern = `^/${regexSegments.join('/')}$`; } - return { regex: pattern, paramNames }; + // Detect if the first parameter is a common i18n prefix segment + // Common patterns: locale, lang, language + const firstParam = paramNames[0]; + const hasOptionalPrefix = + firstParam !== undefined && (firstParam === 'locale' || firstParam === 'lang' || firstParam === 'language'); + + return { regex: pattern, paramNames, hasOptionalPrefix }; } function scanAppDirectory( @@ -116,11 +126,12 @@ function scanAppDirectory( const isDynamic = routePath.includes(':'); if (isDynamic) { - const { regex, paramNames } = buildRegexForDynamicRoute(routePath); + const { regex, paramNames, hasOptionalPrefix } = buildRegexForDynamicRoute(routePath); dynamicRoutes.push({ path: routePath, regex, paramNames, + hasOptionalPrefix, }); } else { staticRoutes.push({ diff --git a/packages/nextjs/src/config/manifest/types.ts b/packages/nextjs/src/config/manifest/types.ts index e3a26adfce2f..0a0946be70f7 100644 --- a/packages/nextjs/src/config/manifest/types.ts +++ b/packages/nextjs/src/config/manifest/types.ts @@ -14,6 +14,11 @@ export type RouteInfo = { * (Optional) The names of dynamic parameters in the route */ paramNames?: string[]; + /** + * (Optional) Indicates if the first segment is an optional prefix (e.g., for i18n routing) + * When true, routes like '/foo' should match '/:locale/foo' patterns + */ + hasOptionalPrefix?: boolean; }; /** From 6b84fe032aeb3e71c6c5a728d1e2cb6a2274c765 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Oct 2025 14:23:50 +0300 Subject: [PATCH 2/8] tests: add tests --- .../test/client/parameterization.test.ts | 257 ++++++++++++++++++ .../suites/base-path/base-path.test.ts | 1 + .../catchall-at-root/catchall-at-root.test.ts | 1 + .../manifest/suites/catchall/catchall.test.ts | 1 + .../manifest/suites/dynamic/dynamic.test.ts | 4 + .../suites/route-groups/route-groups.test.ts | 2 + 6 files changed, 266 insertions(+) diff --git a/packages/nextjs/test/client/parameterization.test.ts b/packages/nextjs/test/client/parameterization.test.ts index e9f484e71827..6b717dede643 100644 --- a/packages/nextjs/test/client/parameterization.test.ts +++ b/packages/nextjs/test/client/parameterization.test.ts @@ -644,4 +644,261 @@ describe('maybeParameterizeRoute', () => { expect(maybeParameterizeRoute('/some/random/path')).toBe('/:catchall*'); }); }); + + describe('i18n routing with optional prefix', () => { + it('should match routes with optional locale prefix for default locale paths', () => { + const manifest: RouteManifest = { + staticRoutes: [{ path: '/' }], + dynamicRoutes: [ + { + path: '/:locale', + regex: '^/([^/]+)$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/foo', + regex: '^/([^/]+)/foo$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/bar', + regex: '^/([^/]+)/bar$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/products', + regex: '^/([^/]+)/products$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Default locale paths (without prefix) should match parameterized routes + expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo'); + expect(maybeParameterizeRoute('/bar')).toBe('/:locale/bar'); + expect(maybeParameterizeRoute('/products')).toBe('/:locale/products'); + + // Non-default locale paths (with prefix) should also match + expect(maybeParameterizeRoute('/ar/foo')).toBe('/:locale/foo'); + expect(maybeParameterizeRoute('/ar/bar')).toBe('/:locale/bar'); + expect(maybeParameterizeRoute('/ar/products')).toBe('/:locale/products'); + expect(maybeParameterizeRoute('/en/foo')).toBe('/:locale/foo'); + expect(maybeParameterizeRoute('/fr/products')).toBe('/:locale/products'); + }); + + it('should handle nested routes with optional locale prefix', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:locale/foo/:id', + regex: '^/([^/]+)/foo/([^/]+)$', + paramNames: ['locale', 'id'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/products/:productId', + regex: '^/([^/]+)/products/([^/]+)$', + paramNames: ['locale', 'productId'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Default locale (no prefix) + expect(maybeParameterizeRoute('/foo/123')).toBe('/:locale/foo/:id'); + expect(maybeParameterizeRoute('/products/abc')).toBe('/:locale/products/:productId'); + + // Non-default locale (with prefix) + expect(maybeParameterizeRoute('/ar/foo/123')).toBe('/:locale/foo/:id'); + expect(maybeParameterizeRoute('/ar/products/abc')).toBe('/:locale/products/:productId'); + expect(maybeParameterizeRoute('/en/foo/456')).toBe('/:locale/foo/:id'); + }); + + it('should prioritize direct matches over optional prefix matches', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/foo/:id', + regex: '^/foo/([^/]+)$', + paramNames: ['id'], + }, + { + path: '/:locale/foo', + regex: '^/([^/]+)/foo$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Direct match should win + expect(maybeParameterizeRoute('/foo/123')).toBe('/foo/:id'); + + // Optional prefix match when direct match isn't available + expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo'); + expect(maybeParameterizeRoute('/ar/foo')).toBe('/:locale/foo'); + }); + + it('should handle lang and language parameters as optional prefixes', () => { + const manifestWithLang: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:lang/page', + regex: '^/([^/]+)/page$', + paramNames: ['lang'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifestWithLang); + expect(maybeParameterizeRoute('/page')).toBe('/:lang/page'); + expect(maybeParameterizeRoute('/en/page')).toBe('/:lang/page'); + + const manifestWithLanguage: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:language/page', + regex: '^/([^/]+)/page$', + paramNames: ['language'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifestWithLanguage); + expect(maybeParameterizeRoute('/page')).toBe('/:language/page'); + expect(maybeParameterizeRoute('/en/page')).toBe('/:language/page'); + }); + + it('should not apply optional prefix logic to non-i18n dynamic segments', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:userId/profile', + regex: '^/([^/]+)/profile$', + paramNames: ['userId'], + hasOptionalPrefix: false, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Should not match without the userId segment + expect(maybeParameterizeRoute('/profile')).toBeUndefined(); + + // Should match with the userId segment + expect(maybeParameterizeRoute('/123/profile')).toBe('/:userId/profile'); + }); + + it('should handle real-world next-intl scenario', () => { + const manifest: RouteManifest = { + staticRoutes: [{ path: '/' }], + dynamicRoutes: [ + { + path: '/:locale', + regex: '^/([^/]+)$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/hola', + regex: '^/([^/]+)/hola$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/products', + regex: '^/([^/]+)/products$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Root should not be parameterized (it's a static route) + expect(maybeParameterizeRoute('/')).toBeUndefined(); + + // Default locale (English, no prefix) - this was the bug + expect(maybeParameterizeRoute('/hola')).toBe('/:locale/hola'); + expect(maybeParameterizeRoute('/products')).toBe('/:locale/products'); + + // Non-default locale (Arabic, with prefix) + expect(maybeParameterizeRoute('/ar')).toBe('/:locale'); + expect(maybeParameterizeRoute('/ar/hola')).toBe('/:locale/hola'); + expect(maybeParameterizeRoute('/ar/products')).toBe('/:locale/products'); + + // Other locales + expect(maybeParameterizeRoute('/en/hola')).toBe('/:locale/hola'); + expect(maybeParameterizeRoute('/fr/products')).toBe('/:locale/products'); + }); + + it('should prefer more specific routes over optional prefix matches', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:locale', + regex: '^/([^/]+)$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/foo/:id', + regex: '^/([^/]+)/foo/([^/]+)$', + paramNames: ['locale', 'id'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/foo', + regex: '^/([^/]+)/foo$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // More specific route should win (specificity score) + expect(maybeParameterizeRoute('/foo/123')).toBe('/:locale/foo/:id'); + expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo'); + expect(maybeParameterizeRoute('/about')).toBe('/:locale'); + }); + + it('should handle deeply nested i18n routes', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:locale/users/:userId/posts/:postId/comments/:commentId', + regex: '^/([^/]+)/users/([^/]+)/posts/([^/]+)/comments/([^/]+)$', + paramNames: ['locale', 'userId', 'postId', 'commentId'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Without locale prefix (default locale) + expect(maybeParameterizeRoute('/users/123/posts/456/comments/789')).toBe( + '/:locale/users/:userId/posts/:postId/comments/:commentId', + ); + + // With locale prefix + expect(maybeParameterizeRoute('/ar/users/123/posts/456/comments/789')).toBe( + '/:locale/users/:userId/posts/:postId/comments/:commentId', + ); + }); + }); }); diff --git a/packages/nextjs/test/config/manifest/suites/base-path/base-path.test.ts b/packages/nextjs/test/config/manifest/suites/base-path/base-path.test.ts index a1014b05c32c..097e3f603693 100644 --- a/packages/nextjs/test/config/manifest/suites/base-path/base-path.test.ts +++ b/packages/nextjs/test/config/manifest/suites/base-path/base-path.test.ts @@ -16,6 +16,7 @@ describe('basePath', () => { path: '/my-app/users/:id', regex: '^/my-app/users/([^/]+)$', paramNames: ['id'], + hasOptionalPrefix: false, }, ], }); diff --git a/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts b/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts index b7108b6f6f23..8d78f24a0986 100644 --- a/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts +++ b/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts @@ -13,6 +13,7 @@ describe('catchall', () => { path: '/:path*?', regex: '^/(.*)$', paramNames: ['path'], + hasOptionalPrefix: false, }, ], }); diff --git a/packages/nextjs/test/config/manifest/suites/catchall/catchall.test.ts b/packages/nextjs/test/config/manifest/suites/catchall/catchall.test.ts index b1c417970ba4..d259a1a38223 100644 --- a/packages/nextjs/test/config/manifest/suites/catchall/catchall.test.ts +++ b/packages/nextjs/test/config/manifest/suites/catchall/catchall.test.ts @@ -13,6 +13,7 @@ describe('catchall', () => { path: '/catchall/:path*?', regex: '^/catchall(?:/(.*))?$', paramNames: ['path'], + hasOptionalPrefix: false, }, ], }); diff --git a/packages/nextjs/test/config/manifest/suites/dynamic/dynamic.test.ts b/packages/nextjs/test/config/manifest/suites/dynamic/dynamic.test.ts index fdcae299d7cf..2ea4b4aca5d8 100644 --- a/packages/nextjs/test/config/manifest/suites/dynamic/dynamic.test.ts +++ b/packages/nextjs/test/config/manifest/suites/dynamic/dynamic.test.ts @@ -13,21 +13,25 @@ describe('dynamic', () => { path: '/dynamic/:id', regex: '^/dynamic/([^/]+)$', paramNames: ['id'], + hasOptionalPrefix: false, }, { path: '/users/:id', regex: '^/users/([^/]+)$', paramNames: ['id'], + hasOptionalPrefix: false, }, { path: '/users/:id/posts/:postId', regex: '^/users/([^/]+)/posts/([^/]+)$', paramNames: ['id', 'postId'], + hasOptionalPrefix: false, }, { path: '/users/:id/settings', regex: '^/users/([^/]+)/settings$', paramNames: ['id'], + hasOptionalPrefix: false, }, ], }); diff --git a/packages/nextjs/test/config/manifest/suites/route-groups/route-groups.test.ts b/packages/nextjs/test/config/manifest/suites/route-groups/route-groups.test.ts index 36ac9077df7e..8e1fe463190e 100644 --- a/packages/nextjs/test/config/manifest/suites/route-groups/route-groups.test.ts +++ b/packages/nextjs/test/config/manifest/suites/route-groups/route-groups.test.ts @@ -23,6 +23,7 @@ describe('route-groups', () => { path: '/dashboard/:id', regex: '^/dashboard/([^/]+)$', paramNames: ['id'], + hasOptionalPrefix: false, }, ], }); @@ -55,6 +56,7 @@ describe('route-groups', () => { path: '/(dashboard)/dashboard/:id', regex: '^/\\(dashboard\\)/dashboard/([^/]+)$', paramNames: ['id'], + hasOptionalPrefix: false, }, ], }); From 194c47ad64661b1b950a8a269d85e64b0d1910ab Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Oct 2025 15:39:20 +0300 Subject: [PATCH 3/8] refactor: put optional prefix check in fn --- .../src/config/manifest/createRouteManifest.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/config/manifest/createRouteManifest.ts b/packages/nextjs/src/config/manifest/createRouteManifest.ts index 8203b855cab5..5e2a99f66285 100644 --- a/packages/nextjs/src/config/manifest/createRouteManifest.ts +++ b/packages/nextjs/src/config/manifest/createRouteManifest.ts @@ -99,13 +99,20 @@ function buildRegexForDynamicRoute(routePath: string): { pattern = `^/${regexSegments.join('/')}$`; } - // Detect if the first parameter is a common i18n prefix segment - // Common patterns: locale, lang, language + return { regex: pattern, paramNames, hasOptionalPrefix: hasOptionalPrefix(paramNames) }; +} + +/** + * Detect if the first parameter is a common i18n prefix segment + * Common patterns: locale, lang, language + */ +function hasOptionalPrefix(paramNames: string[]): boolean { const firstParam = paramNames[0]; - const hasOptionalPrefix = - firstParam !== undefined && (firstParam === 'locale' || firstParam === 'lang' || firstParam === 'language'); + if (firstParam === undefined) { + return false; + } - return { regex: pattern, paramNames, hasOptionalPrefix }; + return firstParam === 'locale' || firstParam === 'lang' || firstParam === 'language'; } function scanAppDirectory( From e1299adc166cc899fea6d3f6e5e129fa2f91dc84 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Oct 2025 17:55:16 +0300 Subject: [PATCH 4/8] tests: added tests --- .../nextjs-15/app/[locale]/i18n-test/page.tsx | 10 +++ .../nextjs-15/tests/i18n-routing.test.ts | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/[locale]/i18n-test/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/[locale]/i18n-test/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/[locale]/i18n-test/page.tsx new file mode 100644 index 000000000000..10c32a944514 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/[locale]/i18n-test/page.tsx @@ -0,0 +1,10 @@ +export default async function I18nTestPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + return ( +
+

I18n Test Page

+

Current locale: {locale || 'default'}

+

This page tests i18n route parameterization

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts new file mode 100644 index 000000000000..e225cee509dd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create consistent parameterized transaction for default locale (no prefix)', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + // Visit route without locale prefix (simulating default locale behavior) + const response = await page.goto(`/i18n-test`); + + // Ensure page loaded successfully + expect(response?.status()).toBe(200); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + }, + }, + transaction: '/:locale/i18n-test', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('should create consistent parameterized transaction for non-default locale (with prefix)', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + // Visit route with locale prefix (simulating non-default locale) + const response = await page.goto(`/ar/i18n-test`); + + // Ensure page loaded successfully + expect(response?.status()).toBe(200); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + }, + }, + transaction: '/:locale/i18n-test', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); From c1f69cedea84fe53ff519b25ce1f3473529ba747 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Oct 2025 18:11:29 +0300 Subject: [PATCH 5/8] fix: test path consistency for now --- .../nextjs-15/tests/i18n-routing.test.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts index e225cee509dd..fda0645fa1a3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts @@ -1,16 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('should create consistent parameterized transaction for default locale (no prefix)', async ({ page }) => { +test('should create consistent parameterized transaction for i18n routes - locale: en', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; }); - // Visit route without locale prefix (simulating default locale behavior) - const response = await page.goto(`/i18n-test`); - - // Ensure page loaded successfully - expect(response?.status()).toBe(200); + await page.goto(`/en/i18n-test`); const transaction = await transactionPromise; @@ -32,16 +28,12 @@ test('should create consistent parameterized transaction for default locale (no }); }); -test('should create consistent parameterized transaction for non-default locale (with prefix)', async ({ page }) => { +test('should create consistent parameterized transaction for i18n routes - locale: ar', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; }); - // Visit route with locale prefix (simulating non-default locale) - const response = await page.goto(`/ar/i18n-test`); - - // Ensure page loaded successfully - expect(response?.status()).toBe(200); + await page.goto(`/ar/i18n-test`); const transaction = await transactionPromise; From 8e894db09951d94a3c8f49c141476471f242d940 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Oct 2025 20:03:01 +0300 Subject: [PATCH 6/8] fix: paramaterization for root route --- .../src/client/routing/parameterization.ts | 3 +- .../test/client/parameterization.test.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/client/routing/parameterization.ts b/packages/nextjs/src/client/routing/parameterization.ts index 32f50c5bbde6..d13097435f41 100644 --- a/packages/nextjs/src/client/routing/parameterization.ts +++ b/packages/nextjs/src/client/routing/parameterization.ts @@ -152,7 +152,8 @@ function findMatchingRoutes( if (dynamicRoute.hasOptionalPrefix && dynamicRoute.regex) { // Prepend a placeholder segment to simulate the optional prefix // e.g., '/foo' becomes '/PLACEHOLDER/foo' to match '/:locale/foo' - const routeWithPrefix = `/SENTRY_OPTIONAL_PREFIX${route}`; + // Special case: '/' becomes '/PLACEHOLDER' (not '/PLACEHOLDER/') to match '/:locale' pattern + const routeWithPrefix = route === '/' ? '/SENTRY_OPTIONAL_PREFIX' : `/SENTRY_OPTIONAL_PREFIX${route}`; const regex = getCompiledRegex(dynamicRoute.regex); if (regex?.test(routeWithPrefix)) { matches.push(dynamicRoute.path); diff --git a/packages/nextjs/test/client/parameterization.test.ts b/packages/nextjs/test/client/parameterization.test.ts index 6b717dede643..e593596aa8c1 100644 --- a/packages/nextjs/test/client/parameterization.test.ts +++ b/packages/nextjs/test/client/parameterization.test.ts @@ -900,5 +900,37 @@ describe('maybeParameterizeRoute', () => { '/:locale/users/:userId/posts/:postId/comments/:commentId', ); }); + + it('should handle root path with optional locale prefix', () => { + const manifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [ + { + path: '/:locale', + regex: '^/([^/]+)$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + { + path: '/:locale/about', + regex: '^/([^/]+)/about$', + paramNames: ['locale'], + hasOptionalPrefix: true, + }, + ], + }; + globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); + + // Root path without locale prefix (default locale) + expect(maybeParameterizeRoute('/')).toBe('/:locale'); + + // Root path with locale prefix + expect(maybeParameterizeRoute('/en')).toBe('/:locale'); + expect(maybeParameterizeRoute('/ar')).toBe('/:locale'); + + // Nested routes still work + expect(maybeParameterizeRoute('/about')).toBe('/:locale/about'); + expect(maybeParameterizeRoute('/fr/about')).toBe('/:locale/about'); + }); }); }); From 20a8d96910d0e8b1e4a8cc9992615491d9d52913 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Oct 2025 20:03:42 +0300 Subject: [PATCH 7/8] tests: added e2e test for next intl --- .../nextjs-15-intl/.gitignore | 51 +++++++++++ .../test-applications/nextjs-15-intl/.npmrc | 2 + .../app/[locale]/i18n-test/page.tsx | 9 ++ .../nextjs-15-intl/app/[locale]/page.tsx | 9 ++ .../nextjs-15-intl/app/layout.tsx | 11 +++ .../nextjs-15-intl/i18n/request.ts | 14 +++ .../nextjs-15-intl/i18n/routing.ts | 10 +++ .../nextjs-15-intl/instrumentation-client.ts | 11 +++ .../nextjs-15-intl/instrumentation.ts | 13 +++ .../nextjs-15-intl/middleware.ts | 8 ++ .../nextjs-15-intl/next.config.js | 11 +++ .../nextjs-15-intl/package.json | 31 +++++++ .../nextjs-15-intl/playwright.config.mjs | 25 ++++++ .../nextjs-15-intl/sentry.edge.config.ts | 9 ++ .../nextjs-15-intl/sentry.server.config.ts | 12 +++ .../nextjs-15-intl/start-event-proxy.mjs | 14 +++ .../nextjs-15-intl/tests/i18n-routing.test.ts | 90 +++++++++++++++++++ .../nextjs-15-intl/tsconfig.json | 40 +++++++++ 18 files changed, 370 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/i18n-test/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/request.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/routing.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation-client.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/tests/i18n-routing.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.gitignore new file mode 100644 index 000000000000..2d0dd371dc86 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.gitignore @@ -0,0 +1,51 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry +.sentryclirc + +pnpm-lock.yaml +.tmp_dev_server_logs +.tmp_build_stdout +.tmp_build_stderr +event-dumps +test-results + diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/i18n-test/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/i18n-test/page.tsx new file mode 100644 index 000000000000..7e2e8d45db06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/i18n-test/page.tsx @@ -0,0 +1,9 @@ +export default async function I18nTestPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + return ( +
+

I18n Test Page

+

Current locale: {locale}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/page.tsx new file mode 100644 index 000000000000..23e7b3213a3f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/[locale]/page.tsx @@ -0,0 +1,9 @@ +export default async function LocaleRootPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + return ( +
+

Locale Root

+

Current locale: {locale}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/layout.tsx new file mode 100644 index 000000000000..60b3740fd7a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/app/layout.tsx @@ -0,0 +1,11 @@ +export const metadata = { + title: 'Next.js 15 i18n Test', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/request.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/request.ts new file mode 100644 index 000000000000..5ed375a9107a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/request.ts @@ -0,0 +1,14 @@ +import { getRequestConfig } from 'next-intl/server'; +import { hasLocale } from 'next-intl'; +import { routing } from './routing'; + +export default getRequestConfig(async ({ requestLocale }) => { + // Typically corresponds to the `[locale]` segment + const requested = await requestLocale; + const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale; + + return { + locale, + messages: {}, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/routing.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/routing.ts new file mode 100644 index 000000000000..efa95881eabc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/i18n/routing.ts @@ -0,0 +1,10 @@ +import { defineRouting } from 'next-intl/routing'; +import { createNavigation } from 'next-intl/navigation'; + +export const routing = defineRouting({ + locales: ['en', 'ar', 'fr'], + defaultLocale: 'en', + localePrefix: 'as-needed', +}); + +export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation-client.ts new file mode 100644 index 000000000000..c232101a75e3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation-client.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/middleware.ts new file mode 100644 index 000000000000..14e2b3ce738a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/middleware.ts @@ -0,0 +1,8 @@ +import createMiddleware from 'next-intl/middleware'; +import { routing } from './i18n/routing'; + +export default createMiddleware(routing); + +export const config = { + matcher: ['/((?!api|_next|.*\\..*).*)'], +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js new file mode 100644 index 000000000000..edd191e14b38 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js @@ -0,0 +1,11 @@ +const { withSentryConfig } = require('@sentry/nextjs'); +const createNextIntlPlugin = require('next-intl/plugin'); + +const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); + +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +module.exports = withSentryConfig(withNextIntl(nextConfig), { + silent: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json new file mode 100644 index 000000000000..359b939eaf50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json @@ -0,0 +1,31 @@ +{ + "name": "nextjs-15-intl", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@types/node": "^18.19.1", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "15.5.4", + "next-intl": "^4.3.12", + "react": "latest", + "react-dom": "latest", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/playwright.config.mjs new file mode 100644 index 000000000000..38548e975851 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/playwright.config.mjs @@ -0,0 +1,25 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const getStartCommand = () => { + if (testEnv === 'development') { + return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs'; + } + + if (testEnv === 'production') { + return 'pnpm next start -p 3030'; + } + + throw new Error(`Unknown test env: ${testEnv}`); +}; + +const config = getPlaywrightConfig({ + startCommand: getStartCommand(), + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.edge.config.ts new file mode 100644 index 000000000000..e9521895498e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.edge.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', + dsn: process.env.SENTRY_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts new file mode 100644 index 000000000000..760b8b581a29 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1.0, + sendDefaultPii: true, + transportOptions: { + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/start-event-proxy.mjs new file mode 100644 index 000000000000..8f6b9b5886d5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/start-event-proxy.mjs @@ -0,0 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-15-intl', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/nextjs-15-intl-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tests/i18n-routing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tests/i18n-routing.test.ts new file mode 100644 index 000000000000..0943df8c7216 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tests/i18n-routing.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create consistent parameterized transaction for default locale without prefix', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15-intl', async transactionEvent => { + return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/i18n-test`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/:locale/i18n-test', + transaction_info: { source: 'route' }, + contexts: { + trace: { + data: { + 'sentry.source': 'route', + }, + }, + }, + }); +}); + +test('should create consistent parameterized transaction for non-default locale with prefix', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15-intl', async transactionEvent => { + return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/ar/i18n-test`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/:locale/i18n-test', + transaction_info: { source: 'route' }, + contexts: { + trace: { + data: { + 'sentry.source': 'route', + }, + }, + }, + }); +}); + +test('should parameterize locale root page correctly for default locale without prefix', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15-intl', async transactionEvent => { + return transactionEvent.transaction === '/:locale' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/:locale', + transaction_info: { source: 'route' }, + contexts: { + trace: { + data: { + 'sentry.source': 'route', + }, + }, + }, + }); +}); + +test('should parameterize locale root page correctly for non-default locale with prefix', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-15-intl', async transactionEvent => { + return transactionEvent.transaction === '/:locale' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/fr`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/:locale', + transaction_info: { source: 'route' }, + contexts: { + trace: { + data: { + 'sentry.source': 'route', + }, + }, + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json new file mode 100644 index 000000000000..d81d4ee14e76 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + }, + "target": "ES2017" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From 89b04f7884789b8d0bf77ca2ac9578b0c06b2409 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Oct 2025 20:30:22 +0300 Subject: [PATCH 8/8] style: formatting --- .../nextjs-15-intl/tsconfig.json | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json index d81d4ee14e76..64c21044c49f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/tsconfig.json @@ -1,10 +1,6 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -22,19 +18,10 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "target": "ES2017" }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }