From 268c230215c91e8930a233b0b45baebb0342462e Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 13:05:37 +0400 Subject: [PATCH 1/8] #42398 test(production): check link to non iso route --- .../app/[slug]/page.js | 12 ++++++ .../app/layout.js | 7 ++++ .../app-dir-prefetch-non-iso-url/app/page.js | 18 +++++++++ .../index.test.ts | 37 +++++++++++++++++++ .../next.config.js | 6 +++ 5 files changed, 80 insertions(+) create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/layout.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/page.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/index.test.ts create mode 100644 test/production/app-dir-prefetch-non-iso-url/next.config.js diff --git a/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js new file mode 100644 index 000000000000000..90ee69c678574b3 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js @@ -0,0 +1,12 @@ +export default function Slug(props) { + return ( + <> +

/[slug]

+

{JSON.stringify(props)}

+ + ) +} + +export function generateStaticParams() { + return [{ slug: 'iso-url' }, { slug: 'кириллица' }] +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/layout.js b/test/production/app-dir-prefetch-non-iso-url/app/layout.js new file mode 100644 index 000000000000000..750eb927b198012 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/layout.js @@ -0,0 +1,7 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/page.js b/test/production/app-dir-prefetch-non-iso-url/app/page.js new file mode 100644 index 000000000000000..786b1ea934a51de --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/page.js @@ -0,0 +1,18 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

index

+

{JSON.stringify(props)}

+ + /iso-url + +
+ + /кириллица + +
+ + ) +} diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts new file mode 100644 index 000000000000000..dfb4c62197989e4 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -0,0 +1,37 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { join } from 'path' +import { BrowserInterface } from '../../lib/browsers/base' +import webdriver from 'next-webdriver' +import { waitFor } from 'next-test-utils' + +describe('app-dir-prefetch-non-iso-url', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'next.config.js': new FileRef(join(__dirname, 'next.config.js')), + app: new FileRef(join(__dirname, 'app')), + }, + }) + }) + afterAll(() => next.destroy()) + + it('should go to non-iso url', async () => { + let browser: BrowserInterface + + try { + browser = await webdriver(next.appPort, '/') + await browser.elementByCss('#to-non-iso').click() + + await waitFor(3000) + + expect(browser.elementByCss('#page')).toBe('/[slug]') + } finally { + if (browser) { + await browser.close() + } + } + }) +}) diff --git a/test/production/app-dir-prefetch-non-iso-url/next.config.js b/test/production/app-dir-prefetch-non-iso-url/next.config.js new file mode 100644 index 000000000000000..426499ce6de355c --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/next.config.js @@ -0,0 +1,6 @@ +/** @type {import("next").NextConfig} */ +const nextConfig = { + experimental: { appDir: true }, +} + +module.exports = nextConfig From 592f8c7f4ee93fc442b8541054458de0d114b7f3 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 13:56:30 +0400 Subject: [PATCH 2/8] #42398 test(production): check link to iso route --- .../index.test.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts index dfb4c62197989e4..28625dbe8164a90 100644 --- a/test/production/app-dir-prefetch-non-iso-url/index.test.ts +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -3,7 +3,6 @@ import { NextInstance } from 'test/lib/next-modes/base' import { join } from 'path' import { BrowserInterface } from '../../lib/browsers/base' import webdriver from 'next-webdriver' -import { waitFor } from 'next-test-utils' describe('app-dir-prefetch-non-iso-url', () => { let next: NextInstance @@ -18,6 +17,22 @@ describe('app-dir-prefetch-non-iso-url', () => { }) afterAll(() => next.destroy()) + it('should go to iso url', async () => { + let browser: BrowserInterface + + try { + browser = await webdriver(next.appPort, '/') + await browser.elementByCss('#to-iso').click() + + const text = await browser.elementByCss('#page').text() + expect(text).toBe('/[slug]') + } finally { + if (browser) { + await browser.close() + } + } + }) + it('should go to non-iso url', async () => { let browser: BrowserInterface @@ -25,9 +40,8 @@ describe('app-dir-prefetch-non-iso-url', () => { browser = await webdriver(next.appPort, '/') await browser.elementByCss('#to-non-iso').click() - await waitFor(3000) - - expect(browser.elementByCss('#page')).toBe('/[slug]') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('/[slug]') } finally { if (browser) { await browser.close() From c030b3eddb1d86a88bb1b6497d3132f1153d49b0 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 15:14:20 +0400 Subject: [PATCH 3/8] #42398 fix(app-router): escape non iso FlightRouterState segment value --- .../client/components/app-router-headers.ts | 31 ++++++ .../next/client/components/app-router.tsx | 23 ++--- .../index.test.ts | 7 ++ test/unit/app-router-headers.test.ts | 95 +++++++++++++++++++ 4 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 test/unit/app-router-headers.test.ts diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts index 1eb2747dd1cd22b..aef43864e0a70ed 100644 --- a/packages/next/client/components/app-router-headers.ts +++ b/packages/next/client/components/app-router-headers.ts @@ -1,5 +1,36 @@ +import { + DynamicParamTypesShort, + FlightRouterState, +} from '../../server/app-render' + export const RSC = 'RSC' as const export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const export const RSC_VARY_HEADER = `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const + +export const escapeFlightRouterState = ( + state: FlightRouterState +): FlightRouterState => { + const [segment, parallelRoutes, ...restState] = state + const escapedParallelRoutes: typeof parallelRoutes = Object.create({}) + let escapedSegment: typeof segment = segment + + if (typeof segment !== 'string') { + const [param, value, type] = segment + const escapedSegmentValue = encodeURIComponent(value) + escapedSegment = [ + param, + escapedSegmentValue, + type as DynamicParamTypesShort, + ] + } + + Object.keys(parallelRoutes).forEach((key) => { + const childState = parallelRoutes[key] + + escapedParallelRoutes[key] = escapeFlightRouterState(childState) + }) + + return [escapedSegment, escapedParallelRoutes, ...restState] +} diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index d29f680d5051c63..5788d81e507f952 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -1,18 +1,18 @@ 'use client' import type { ReactNode } from 'react' -import React, { useEffect, useMemo, useCallback } from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack/client' +import type { + AppRouterInstance, + CacheNode, +} from '../../shared/lib/app-router-context' import { AppRouterContext, - LayoutRouterContext, GlobalLayoutRouterContext, + LayoutRouterContext, } from '../../shared/lib/app-router-context' -import type { - CacheNode, - AppRouterInstance, -} from '../../shared/lib/app-router-context' -import type { FlightRouterState, FlightData } from '../../server/app-render' +import type { FlightData, FlightRouterState } from '../../server/app-render' import { ACTION_NAVIGATE, ACTION_PREFETCH, @@ -22,14 +22,13 @@ import { reducer, } from './reducer' import { - SearchParamsContext, - // ParamsContext, PathnameContext, - // LayoutSegmentsContext, + SearchParamsContext, } from '../../shared/lib/hooks-client-context' import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' import { ErrorBoundary, GlobalErrorComponent } from './error-boundary' import { + escapeFlightRouterState, NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, RSC, @@ -65,7 +64,9 @@ export async function fetchServerResponse( // Enable flight response [RSC]: '1', // Provide the current router state - [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState), + [NEXT_ROUTER_STATE_TREE]: JSON.stringify( + escapeFlightRouterState(flightRouterState) + ), } if (prefetch) { // Enable prefetch response diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts index 28625dbe8164a90..6ac83c44517e801 100644 --- a/test/production/app-dir-prefetch-non-iso-url/index.test.ts +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -3,6 +3,7 @@ import { NextInstance } from 'test/lib/next-modes/base' import { join } from 'path' import { BrowserInterface } from '../../lib/browsers/base' import webdriver from 'next-webdriver' +import { waitFor } from 'next-test-utils' describe('app-dir-prefetch-non-iso-url', () => { let next: NextInstance @@ -25,6 +26,9 @@ describe('app-dir-prefetch-non-iso-url', () => { await browser.elementByCss('#to-iso').click() const text = await browser.elementByCss('#page').text() + + await waitFor(3000) + expect(text).toBe('/[slug]') } finally { if (browser) { @@ -41,6 +45,9 @@ describe('app-dir-prefetch-non-iso-url', () => { await browser.elementByCss('#to-non-iso').click() const text = await browser.elementByCss('#page').text() + + await waitFor(3000) + expect(text).toBe('/[slug]') } finally { if (browser) { diff --git a/test/unit/app-router-headers.test.ts b/test/unit/app-router-headers.test.ts new file mode 100644 index 000000000000000..cfd2e3e3579e4e5 --- /dev/null +++ b/test/unit/app-router-headers.test.ts @@ -0,0 +1,95 @@ +import { escapeFlightRouterState } from 'next/client/components/app-router-headers' +import { FlightRouterState } from 'next/server/app-render' + +const state1: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + 'refetch', +] +const state1Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + 'refetch', +] + +const state2: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + null, + true, +] +const state2Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + null, + true, +] + +const state3: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + null, + 'refetch', + ], + }, +] +const state3Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + null, + 'refetch', + ], + }, +] + +describe('escapeFlightRouterState', () => { + it.each([ + { state: state1, escapedState: state1Escaped }, + { state: state2, escapedState: state2Escaped }, + { state: state3, escapedState: state3Escaped }, + ])('should escape non-iso segment value', ({ state, escapedState }) => { + expect(escapeFlightRouterState(state)).toMatchObject(escapedState) + }) +}) From 2616c52f47bfdd4e188cd4e82709c58edfc43c41 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 13:05:37 +0400 Subject: [PATCH 4/8] #42398 test(production): check link to non iso route --- .../app/[slug]/page.js | 12 ++++++ .../app/layout.js | 7 ++++ .../app-dir-prefetch-non-iso-url/app/page.js | 18 +++++++++ .../index.test.ts | 37 +++++++++++++++++++ .../next.config.js | 6 +++ 5 files changed, 80 insertions(+) create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/layout.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/app/page.js create mode 100644 test/production/app-dir-prefetch-non-iso-url/index.test.ts create mode 100644 test/production/app-dir-prefetch-non-iso-url/next.config.js diff --git a/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js new file mode 100644 index 000000000000000..90ee69c678574b3 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js @@ -0,0 +1,12 @@ +export default function Slug(props) { + return ( + <> +

/[slug]

+

{JSON.stringify(props)}

+ + ) +} + +export function generateStaticParams() { + return [{ slug: 'iso-url' }, { slug: 'кириллица' }] +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/layout.js b/test/production/app-dir-prefetch-non-iso-url/app/layout.js new file mode 100644 index 000000000000000..750eb927b198012 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/layout.js @@ -0,0 +1,7 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/page.js b/test/production/app-dir-prefetch-non-iso-url/app/page.js new file mode 100644 index 000000000000000..786b1ea934a51de --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/page.js @@ -0,0 +1,18 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +

index

+

{JSON.stringify(props)}

+ + /iso-url + +
+ + /кириллица + +
+ + ) +} diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts new file mode 100644 index 000000000000000..dfb4c62197989e4 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -0,0 +1,37 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { join } from 'path' +import { BrowserInterface } from '../../lib/browsers/base' +import webdriver from 'next-webdriver' +import { waitFor } from 'next-test-utils' + +describe('app-dir-prefetch-non-iso-url', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'next.config.js': new FileRef(join(__dirname, 'next.config.js')), + app: new FileRef(join(__dirname, 'app')), + }, + }) + }) + afterAll(() => next.destroy()) + + it('should go to non-iso url', async () => { + let browser: BrowserInterface + + try { + browser = await webdriver(next.appPort, '/') + await browser.elementByCss('#to-non-iso').click() + + await waitFor(3000) + + expect(browser.elementByCss('#page')).toBe('/[slug]') + } finally { + if (browser) { + await browser.close() + } + } + }) +}) diff --git a/test/production/app-dir-prefetch-non-iso-url/next.config.js b/test/production/app-dir-prefetch-non-iso-url/next.config.js new file mode 100644 index 000000000000000..426499ce6de355c --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/next.config.js @@ -0,0 +1,6 @@ +/** @type {import("next").NextConfig} */ +const nextConfig = { + experimental: { appDir: true }, +} + +module.exports = nextConfig From 6e3d969766c0cb2c539c3606a83d33e32deaf112 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 13:56:30 +0400 Subject: [PATCH 5/8] #42398 test(production): check link to iso route --- .../index.test.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts index dfb4c62197989e4..28625dbe8164a90 100644 --- a/test/production/app-dir-prefetch-non-iso-url/index.test.ts +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -3,7 +3,6 @@ import { NextInstance } from 'test/lib/next-modes/base' import { join } from 'path' import { BrowserInterface } from '../../lib/browsers/base' import webdriver from 'next-webdriver' -import { waitFor } from 'next-test-utils' describe('app-dir-prefetch-non-iso-url', () => { let next: NextInstance @@ -18,6 +17,22 @@ describe('app-dir-prefetch-non-iso-url', () => { }) afterAll(() => next.destroy()) + it('should go to iso url', async () => { + let browser: BrowserInterface + + try { + browser = await webdriver(next.appPort, '/') + await browser.elementByCss('#to-iso').click() + + const text = await browser.elementByCss('#page').text() + expect(text).toBe('/[slug]') + } finally { + if (browser) { + await browser.close() + } + } + }) + it('should go to non-iso url', async () => { let browser: BrowserInterface @@ -25,9 +40,8 @@ describe('app-dir-prefetch-non-iso-url', () => { browser = await webdriver(next.appPort, '/') await browser.elementByCss('#to-non-iso').click() - await waitFor(3000) - - expect(browser.elementByCss('#page')).toBe('/[slug]') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('/[slug]') } finally { if (browser) { await browser.close() From 3c77a344625de694c42a347641ac57be5f1093a7 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Tue, 8 Nov 2022 15:14:20 +0400 Subject: [PATCH 6/8] #42398 fix(app-router): escape non iso FlightRouterState segment value --- .../client/components/app-router-headers.ts | 31 ++++++ .../next/client/components/app-router.tsx | 23 ++--- .../index.test.ts | 7 ++ test/unit/app-router-headers.test.ts | 95 +++++++++++++++++++ 4 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 test/unit/app-router-headers.test.ts diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts index 1eb2747dd1cd22b..aef43864e0a70ed 100644 --- a/packages/next/client/components/app-router-headers.ts +++ b/packages/next/client/components/app-router-headers.ts @@ -1,5 +1,36 @@ +import { + DynamicParamTypesShort, + FlightRouterState, +} from '../../server/app-render' + export const RSC = 'RSC' as const export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const export const RSC_VARY_HEADER = `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const + +export const escapeFlightRouterState = ( + state: FlightRouterState +): FlightRouterState => { + const [segment, parallelRoutes, ...restState] = state + const escapedParallelRoutes: typeof parallelRoutes = Object.create({}) + let escapedSegment: typeof segment = segment + + if (typeof segment !== 'string') { + const [param, value, type] = segment + const escapedSegmentValue = encodeURIComponent(value) + escapedSegment = [ + param, + escapedSegmentValue, + type as DynamicParamTypesShort, + ] + } + + Object.keys(parallelRoutes).forEach((key) => { + const childState = parallelRoutes[key] + + escapedParallelRoutes[key] = escapeFlightRouterState(childState) + }) + + return [escapedSegment, escapedParallelRoutes, ...restState] +} diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index d29f680d5051c63..5788d81e507f952 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -1,18 +1,18 @@ 'use client' import type { ReactNode } from 'react' -import React, { useEffect, useMemo, useCallback } from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack/client' +import type { + AppRouterInstance, + CacheNode, +} from '../../shared/lib/app-router-context' import { AppRouterContext, - LayoutRouterContext, GlobalLayoutRouterContext, + LayoutRouterContext, } from '../../shared/lib/app-router-context' -import type { - CacheNode, - AppRouterInstance, -} from '../../shared/lib/app-router-context' -import type { FlightRouterState, FlightData } from '../../server/app-render' +import type { FlightData, FlightRouterState } from '../../server/app-render' import { ACTION_NAVIGATE, ACTION_PREFETCH, @@ -22,14 +22,13 @@ import { reducer, } from './reducer' import { - SearchParamsContext, - // ParamsContext, PathnameContext, - // LayoutSegmentsContext, + SearchParamsContext, } from '../../shared/lib/hooks-client-context' import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' import { ErrorBoundary, GlobalErrorComponent } from './error-boundary' import { + escapeFlightRouterState, NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, RSC, @@ -65,7 +64,9 @@ export async function fetchServerResponse( // Enable flight response [RSC]: '1', // Provide the current router state - [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState), + [NEXT_ROUTER_STATE_TREE]: JSON.stringify( + escapeFlightRouterState(flightRouterState) + ), } if (prefetch) { // Enable prefetch response diff --git a/test/production/app-dir-prefetch-non-iso-url/index.test.ts b/test/production/app-dir-prefetch-non-iso-url/index.test.ts index 28625dbe8164a90..6ac83c44517e801 100644 --- a/test/production/app-dir-prefetch-non-iso-url/index.test.ts +++ b/test/production/app-dir-prefetch-non-iso-url/index.test.ts @@ -3,6 +3,7 @@ import { NextInstance } from 'test/lib/next-modes/base' import { join } from 'path' import { BrowserInterface } from '../../lib/browsers/base' import webdriver from 'next-webdriver' +import { waitFor } from 'next-test-utils' describe('app-dir-prefetch-non-iso-url', () => { let next: NextInstance @@ -25,6 +26,9 @@ describe('app-dir-prefetch-non-iso-url', () => { await browser.elementByCss('#to-iso').click() const text = await browser.elementByCss('#page').text() + + await waitFor(3000) + expect(text).toBe('/[slug]') } finally { if (browser) { @@ -41,6 +45,9 @@ describe('app-dir-prefetch-non-iso-url', () => { await browser.elementByCss('#to-non-iso').click() const text = await browser.elementByCss('#page').text() + + await waitFor(3000) + expect(text).toBe('/[slug]') } finally { if (browser) { diff --git a/test/unit/app-router-headers.test.ts b/test/unit/app-router-headers.test.ts new file mode 100644 index 000000000000000..cfd2e3e3579e4e5 --- /dev/null +++ b/test/unit/app-router-headers.test.ts @@ -0,0 +1,95 @@ +import { escapeFlightRouterState } from 'next/client/components/app-router-headers' +import { FlightRouterState } from 'next/server/app-render' + +const state1: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + 'refetch', +] +const state1Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + 'refetch', +] + +const state2: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + null, + true, +] +const state2Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + ], + }, + null, + null, + true, +] + +const state3: FlightRouterState = [ + '', + { + children: [ + ['slug', 'свитер-urbain', 'd'], + { + children: ['', {}], + }, + null, + 'refetch', + ], + }, +] +const state3Escaped: FlightRouterState = [ + '', + { + children: [ + ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], + { + children: ['', {}], + }, + null, + 'refetch', + ], + }, +] + +describe('escapeFlightRouterState', () => { + it.each([ + { state: state1, escapedState: state1Escaped }, + { state: state2, escapedState: state2Escaped }, + { state: state3, escapedState: state3Escaped }, + ])('should escape non-iso segment value', ({ state, escapedState }) => { + expect(escapeFlightRouterState(state)).toMatchObject(escapedState) + }) +}) From c0a04806e0bf0d0cab81ffe814ab02c6c5627b38 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Sat, 12 Nov 2022 10:27:01 +0400 Subject: [PATCH 7/8] #42398 fix(app-render): escape segment value --- .../client/components/app-router-headers.ts | 31 ------ .../next/client/components/app-router.tsx | 23 +++-- packages/next/server/app-render.tsx | 8 +- test/unit/app-router-headers.test.ts | 95 ------------------- 4 files changed, 18 insertions(+), 139 deletions(-) delete mode 100644 test/unit/app-router-headers.test.ts diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts index aef43864e0a70ed..1eb2747dd1cd22b 100644 --- a/packages/next/client/components/app-router-headers.ts +++ b/packages/next/client/components/app-router-headers.ts @@ -1,36 +1,5 @@ -import { - DynamicParamTypesShort, - FlightRouterState, -} from '../../server/app-render' - export const RSC = 'RSC' as const export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const export const RSC_VARY_HEADER = `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const - -export const escapeFlightRouterState = ( - state: FlightRouterState -): FlightRouterState => { - const [segment, parallelRoutes, ...restState] = state - const escapedParallelRoutes: typeof parallelRoutes = Object.create({}) - let escapedSegment: typeof segment = segment - - if (typeof segment !== 'string') { - const [param, value, type] = segment - const escapedSegmentValue = encodeURIComponent(value) - escapedSegment = [ - param, - escapedSegmentValue, - type as DynamicParamTypesShort, - ] - } - - Object.keys(parallelRoutes).forEach((key) => { - const childState = parallelRoutes[key] - - escapedParallelRoutes[key] = escapeFlightRouterState(childState) - }) - - return [escapedSegment, escapedParallelRoutes, ...restState] -} diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index 5788d81e507f952..d29f680d5051c63 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -1,18 +1,18 @@ 'use client' import type { ReactNode } from 'react' -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useEffect, useMemo, useCallback } from 'react' import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack/client' -import type { - AppRouterInstance, - CacheNode, -} from '../../shared/lib/app-router-context' import { AppRouterContext, - GlobalLayoutRouterContext, LayoutRouterContext, + GlobalLayoutRouterContext, } from '../../shared/lib/app-router-context' -import type { FlightData, FlightRouterState } from '../../server/app-render' +import type { + CacheNode, + AppRouterInstance, +} from '../../shared/lib/app-router-context' +import type { FlightRouterState, FlightData } from '../../server/app-render' import { ACTION_NAVIGATE, ACTION_PREFETCH, @@ -22,13 +22,14 @@ import { reducer, } from './reducer' import { - PathnameContext, SearchParamsContext, + // ParamsContext, + PathnameContext, + // LayoutSegmentsContext, } from '../../shared/lib/hooks-client-context' import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' import { ErrorBoundary, GlobalErrorComponent } from './error-boundary' import { - escapeFlightRouterState, NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, RSC, @@ -64,9 +65,7 @@ export async function fetchServerResponse( // Enable flight response [RSC]: '1', // Provide the current router state - [NEXT_ROUTER_STATE_TREE]: JSON.stringify( - escapeFlightRouterState(flightRouterState) - ), + [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState), } if (prefetch) { // Enable prefetch response diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index d5fd2404538d9b1..8f122e0790cda26 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -855,7 +855,13 @@ export async function renderToHTMLOrFlight( } const key = segmentParam.param - const value = pathParams[key] + let value = pathParams[key] + + if (Array.isArray(value)) { + value = value.map((i) => encodeURIComponent(i)) + } else if (typeof value === 'string') { + value = encodeURIComponent(value) + } if (!value) { // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` diff --git a/test/unit/app-router-headers.test.ts b/test/unit/app-router-headers.test.ts deleted file mode 100644 index cfd2e3e3579e4e5..000000000000000 --- a/test/unit/app-router-headers.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { escapeFlightRouterState } from 'next/client/components/app-router-headers' -import { FlightRouterState } from 'next/server/app-render' - -const state1: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - 'refetch', -] -const state1Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - 'refetch', -] - -const state2: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - null, - true, -] -const state2Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - null, - true, -] - -const state3: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - null, - 'refetch', - ], - }, -] -const state3Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - null, - 'refetch', - ], - }, -] - -describe('escapeFlightRouterState', () => { - it.each([ - { state: state1, escapedState: state1Escaped }, - { state: state2, escapedState: state2Escaped }, - { state: state3, escapedState: state3Escaped }, - ])('should escape non-iso segment value', ({ state, escapedState }) => { - expect(escapeFlightRouterState(state)).toMatchObject(escapedState) - }) -}) From 7993357e84faa2576f428f656ba04d607e56e633 Mon Sep 17 00:00:00 2001 From: Marcus-Rise Date: Sat, 12 Nov 2022 10:29:51 +0400 Subject: [PATCH 8/8] merge conflicts --- test/unit/app-router-headers.test.ts | 95 ---------------------------- 1 file changed, 95 deletions(-) delete mode 100644 test/unit/app-router-headers.test.ts diff --git a/test/unit/app-router-headers.test.ts b/test/unit/app-router-headers.test.ts deleted file mode 100644 index cfd2e3e3579e4e5..000000000000000 --- a/test/unit/app-router-headers.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { escapeFlightRouterState } from 'next/client/components/app-router-headers' -import { FlightRouterState } from 'next/server/app-render' - -const state1: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - 'refetch', -] -const state1Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - 'refetch', -] - -const state2: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - null, - true, -] -const state2Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - ], - }, - null, - null, - true, -] - -const state3: FlightRouterState = [ - '', - { - children: [ - ['slug', 'свитер-urbain', 'd'], - { - children: ['', {}], - }, - null, - 'refetch', - ], - }, -] -const state3Escaped: FlightRouterState = [ - '', - { - children: [ - ['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'], - { - children: ['', {}], - }, - null, - 'refetch', - ], - }, -] - -describe('escapeFlightRouterState', () => { - it.each([ - { state: state1, escapedState: state1Escaped }, - { state: state2, escapedState: state2Escaped }, - { state: state3, escapedState: state3Escaped }, - ])('should escape non-iso segment value', ({ state, escapedState }) => { - expect(escapeFlightRouterState(state)).toMatchObject(escapedState) - }) -})