From 1e1f36fd19e3d5288e63aa0a09a0ea0c10cf81b2 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 4 Dec 2020 00:47:07 -0600 Subject: [PATCH 1/8] Add required server files and minimal handling --- packages/next/build/index.ts | 52 +++++++ packages/next/next-server/lib/constants.ts | 1 + .../next/next-server/server/next-server.ts | 10 +- .../pages/dynamic/[slug].js | 19 +++ .../pages/fallback/[slug].js | 26 ++++ .../required-server-files/pages/index.js | 17 ++ .../required-server-files/test/index.test.js | 147 ++++++++++++++++++ 7 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 test/integration/required-server-files/pages/dynamic/[slug].js create mode 100644 test/integration/required-server-files/pages/fallback/[slug].js create mode 100644 test/integration/required-server-files/pages/index.js create mode 100644 test/integration/required-server-files/test/index.test.js diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5e51c99d35b14..ab3c29a6cbf35 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -25,17 +25,21 @@ import { loadEnvConfig } from '@next/env' import { recursiveDelete } from '../lib/recursive-delete' import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup' import { + BUILD_ID_FILE, BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, EXPORT_DETAIL, EXPORT_MARKER, + FONT_MANIFEST, IMAGES_MANIFEST, PAGES_MANIFEST, PHASE_PRODUCTION_BUILD, PRERENDER_MANIFEST, + REACT_LOADABLE_MANIFEST, ROUTES_MANIFEST, SERVERLESS_DIRECTORY, SERVER_DIRECTORY, + SERVER_FILES_MANIFEST, } from '../next-server/lib/constants' import { getRouteRegex, @@ -361,6 +365,27 @@ export default async function build( 'utf8' ) + const requiredServerFiles = { + version: 1, + files: [ + ...[ + ROUTES_MANIFEST, + PAGES_MANIFEST, + BUILD_MANIFEST, + PRERENDER_MANIFEST, + REACT_LOADABLE_MANIFEST, + FONT_MANIFEST, + BUILD_ID_FILE, + ].map((file) => path.join(config.distDir, file)), + ], + ignore: [ + path.relative( + dir, + path.join(path.dirname(require.resolve('sharp')), '**/*') + ), + ], + } + const configs = await Promise.all([ getBaseWebpackConfig(dir, { tracer, @@ -533,6 +558,8 @@ export default async function build( false )) + let hasSsrAmpPages = false + const analysisBegin = process.hrtime() await Promise.all( pageKeys.map(async (page) => { @@ -593,6 +620,13 @@ export default async function build( config.i18n?.defaultLocale ) + if ( + workerResult.isStatic === false && + (workerResult.isHybridAmp || workerResult.isAmpOnly) + ) { + hasSsrAmpPages = true + } + if (workerResult.isHybridAmp) { isHybridAmp = true hybridAmpPages.add(page) @@ -655,6 +689,18 @@ export default async function build( ) staticCheckWorkers.end() + if (!hasSsrAmpPages) { + requiredServerFiles.ignore.push( + path.relative( + dir, + path.join( + path.dirname(require.resolve('@ampproject/toolbox-optimizer')), + '**/*' + ) + ) + ) + } + if (serverPropsPages.size > 0 || ssgPages.size > 0) { // We update the routes manifest after the build with the // data routes since we can't determine these until after build @@ -730,6 +776,12 @@ export default async function build( await writeBuildId(distDir, buildId) + await promises.writeFile( + path.join(distDir, SERVER_FILES_MANIFEST), + JSON.stringify(requiredServerFiles), + 'utf8' + ) + const finalPrerenderRoutes: { [route: string]: SsgRoute } = {} const tbdPrerenderRoutes: string[] = [] let ssgNotFoundPaths: string[] = [] diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts index 09e9e20aeb16c..03a4fc100705b 100644 --- a/packages/next/next-server/lib/constants.ts +++ b/packages/next/next-server/lib/constants.ts @@ -9,6 +9,7 @@ export const EXPORT_DETAIL = 'export-detail.json' export const PRERENDER_MANIFEST = 'prerender-manifest.json' export const ROUTES_MANIFEST = 'routes-manifest.json' export const IMAGES_MANIFEST = 'images-manifest.json' +export const SERVER_FILES_MANIFEST = 'required-server-files.json' export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json' export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json' export const FONT_MANIFEST = 'font-manifest.json' diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 029f9d2db040e..dc019aa65dbb7 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -135,6 +135,7 @@ export default class Server { serverBuildDir: string pagesManifest?: PagesManifest buildId: string + minimalMode: boolean renderOpts: { poweredByHeader: boolean buildId: string @@ -168,8 +169,9 @@ export default class Server { quiet = false, conf = null, dev = false, + minimalMode = false, customServer = true, - }: ServerConstructor = {}) { + }: ServerConstructor & { minimalMode?: boolean } = {}) { this.dir = resolve(dir) this.quiet = quiet const phase = this.currentPhase() @@ -191,6 +193,7 @@ export default class Server { } = this.nextConfig this.buildId = this.readBuildId() + this.minimalMode = minimalMode this.renderOpts = { poweredByHeader: this.nextConfig.poweredByHeader, @@ -261,8 +264,8 @@ export default class Server { this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, 'pages' ), - flushToDisk: this.nextConfig.experimental.sprFlushToDisk, locales: this.nextConfig.i18n?.locales, + flushToDisk: !minimalMode && this.nextConfig.experimental.sprFlushToDisk, }) /** @@ -1357,7 +1360,7 @@ export default class Server { } let ssgCacheKey = - isPreviewMode || !isSSG + isPreviewMode || !isSSG || this.minimalMode ? undefined // Preview mode bypasses the cache : `${locale ? `/${locale}` : ''}${ (pathname === '/' || resolvedUrlPathname === '/') && locale @@ -1540,6 +1543,7 @@ export default class Server { // getStaticPaths, then finish the data request on the client-side. // if ( + this.minimalMode !== true && fallbackMode !== 'blocking' && ssgCacheKey && !didRespond && diff --git a/test/integration/required-server-files/pages/dynamic/[slug].js b/test/integration/required-server-files/pages/dynamic/[slug].js new file mode 100644 index 0000000000000..70e434ce330c4 --- /dev/null +++ b/test/integration/required-server-files/pages/dynamic/[slug].js @@ -0,0 +1,19 @@ +export const getServerSideProps = ({ params }) => { + return { + props: { + hello: 'world', + slug: params.slug, + random: Math.random(), + }, + } +} + +export default function Page(props) { + return ( + <> +

dynamic page

+

{props.slug}

+

{JSON.stringify(props)}

+ + ) +} diff --git a/test/integration/required-server-files/pages/fallback/[slug].js b/test/integration/required-server-files/pages/fallback/[slug].js new file mode 100644 index 0000000000000..6cd7a0c19470e --- /dev/null +++ b/test/integration/required-server-files/pages/fallback/[slug].js @@ -0,0 +1,26 @@ +export const getStaticProps = ({ params }) => { + return { + props: { + hello: 'world', + slug: params.slug, + random: Math.random(), + }, + } +} + +export const getStaticPaths = () => { + return { + paths: ['/fallback/first'], + fallback: true, + } +} + +export default function Page(props) { + return ( + <> +

fallback page

+

{props.slug}

+

{JSON.stringify(props)}

+ + ) +} diff --git a/test/integration/required-server-files/pages/index.js b/test/integration/required-server-files/pages/index.js new file mode 100644 index 0000000000000..29115ad174773 --- /dev/null +++ b/test/integration/required-server-files/pages/index.js @@ -0,0 +1,17 @@ +export const getServerSideProps = () => { + return { + props: { + hello: 'world', + random: Math.random(), + }, + } +} + +export default function Page(props) { + return ( + <> +

index page

+

{JSON.stringify(props)}

+ + ) +} diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js new file mode 100644 index 0000000000000..a51d7ad7f9c0f --- /dev/null +++ b/test/integration/required-server-files/test/index.test.js @@ -0,0 +1,147 @@ +/* eslint-env jest */ + +import http from 'http' +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import Server from 'next/dist/next-server/server/next-server' +import { findPort, nextBuild, renderViaHTTP } from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '..') +let server +let nextApp +let appPort +let buildId +let requiredFilesManifest + +describe('Required Server Files', () => { + beforeAll(async () => { + await fs.remove(join(appDir, '.next')) + await nextBuild(appDir) + + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + requiredFilesManifest = await fs.readJSON( + join(appDir, '.next/required-server-files.json') + ) + + let files = await fs.readdir(join(appDir, '.next')) + + for (const file of files) { + if ( + file === 'server' || + file === 'required-server-files.json' || + requiredFilesManifest.files.includes(join('.next', file)) + ) { + continue + } + console.log('removing', join('.next', file)) + await fs.remove(join(appDir, '.next', file)) + } + await fs.rename(join(appDir, 'pages'), join(appDir, 'pages-bak')) + + nextApp = new Server({ + conf: {}, + dir: appDir, + quiet: false, + minimalMode: true, + }) + appPort = await findPort() + + server = http.createServer(async (req, res) => { + try { + await nextApp.getRequestHandler()(req, res) + } catch (err) { + console.error(err) + res.statusCode = 500 + res.end('error') + } + }) + await new Promise((res, rej) => { + server.listen(appPort, (err) => (err ? rej(err) : res())) + }) + console.log(`Listening at ::${appPort}`) + }) + afterAll(async () => { + if (server) server.close() + await fs.rename(join(appDir, 'pages-bak'), join(appDir, 'pages')) + }) + + it('should output required-server-files manifest correctly', async () => { + expect(requiredFilesManifest.version).toBe(1) + expect(Array.isArray(requiredFilesManifest.files)).toBe(true) + expect(Array.isArray(requiredFilesManifest.ignore)).toBe(true) + expect(requiredFilesManifest.files.length).toBeGreaterThan(0) + expect(requiredFilesManifest.ignore.length).toBeGreaterThan(0) + }) + + it('should render SSR page correctly', async () => { + const html = await renderViaHTTP(appPort, '/') + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#index').text()).toBe('index page') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/') + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#index').text()).toBe('index page') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + }) + + it('should render dynamic SSR page correctly', async () => { + const html = await renderViaHTTP(appPort, '/dynamic/first') + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#dynamic').text()).toBe('dynamic page') + expect($('#slug').text()).toBe('first') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/dynamic/second') + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#dynamic').text()).toBe('dynamic page') + expect($2('#slug').text()).toBe('second') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + }) + + it('should render fallback page correctly', async () => { + const html = await renderViaHTTP(appPort, '/fallback/first') + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#fallback').text()).toBe('fallback page') + expect($('#slug').text()).toBe('first') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/fallback/first') + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#fallback').text()).toBe('fallback page') + expect($2('#slug').text()).toBe('first') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + + const html3 = await renderViaHTTP(appPort, '/fallback/second') + const $3 = cheerio.load(html3) + const data3 = JSON.parse($3('#props').text()) + + expect($3('#fallback').text()).toBe('fallback page') + expect($3('#slug').text()).toBe('second') + expect(isNaN(data3.random)).toBe(false) + + const { pageProps: data4 } = JSON.parse( + await renderViaHTTP(appPort, `/_next/data/${buildId}/fallback/third.json`) + ) + expect(data4.hello).toBe('world') + expect(data4.slug).toBe('third') + }) +}) From fbde83b5d0708d83bb98c081c8043804a007f80a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 4 Dec 2020 09:17:01 -0600 Subject: [PATCH 2/8] Add config to required server files --- packages/next/build/index.ts | 4 ++++ test/integration/required-server-files/test/index.test.js | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index ab3c29a6cbf35..1ccc28ee162ed 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -367,6 +367,10 @@ export default async function build( const requiredServerFiles = { version: 1, + config: { + ...config, + compress: false, + }, files: [ ...[ ROUTES_MANIFEST, diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index a51d7ad7f9c0f..2cd45aa2dcfca 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -74,6 +74,7 @@ describe('Required Server Files', () => { expect(Array.isArray(requiredFilesManifest.ignore)).toBe(true) expect(requiredFilesManifest.files.length).toBeGreaterThan(0) expect(requiredFilesManifest.ignore.length).toBeGreaterThan(0) + expect(typeof requiredFilesManifest.config.trailingSlash).toBe('boolean') }) it('should render SSR page correctly', async () => { From 9dded89a80c4bc7314f2c3f6059334608f4f8dd9 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 4 Dec 2020 19:47:54 -0600 Subject: [PATCH 3/8] Handle matched path --- .../next-serverless-loader/page-handler.ts | 96 +------------- .../loaders/next-serverless-loader/utils.ts | 121 +++++++++++++++++- .../next/next-server/server/next-server.ts | 57 +++++++++ .../required-server-files/test/index.test.js | 81 ++++++++++++ 4 files changed, 264 insertions(+), 91 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 4f7ce8b3cf0c3..9a2399d7bb41a 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -1,4 +1,3 @@ -import { parse as parseQs } from 'querystring' import { IncomingMessage, ServerResponse } from 'http' import { parse as parseUrl, format as formatUrl, UrlWithParsedQuery } from 'url' import { isResSent } from '../../../../next-server/lib/utils' @@ -14,7 +13,6 @@ import { } from '../../../../next-server/server/api-utils' import { getRedirectStatus } from '../../../../lib/load-custom-routes' import getRouteNoAssetPath from '../../../../next-server/lib/router/utils/get-route-from-asset-path' -import { getRouteMatcher } from '../../../../next-server/lib/router/utils/route-matcher' import { PERMANENT_REDIRECT_STATUS } from '../../../../next-server/lib/constants' export function getPageHandler(ctx: ServerlessHandlerCtx) { @@ -56,6 +54,8 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { handleBasePath, defaultRouteRegex, dynamicRouteMatcher, + interpolateDynamicPath, + getParamsFromRouteMatches, normalizeDynamicRouteParams, } = getUtils(ctx) @@ -222,76 +222,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { !hasValidParams && req.headers?.['x-now-route-matches'] ) { - nowParams = getRouteMatcher( - (function () { - const { groups, routeKeys } = defaultRouteRegex! - - return { - re: { - // Simulate a RegExp match from the \`req.url\` input - exec: (str: string) => { - const obj = parseQs(str) - - // favor named matches if available - const routeKeyNames = Object.keys(routeKeys || {}) - - const filterLocaleItem = (val: string | string[]) => { - if (i18n) { - // locale items can be included in route-matches - // for fallback SSG pages so ensure they are - // filtered - const isCatchAll = Array.isArray(val) - const _val = isCatchAll ? val[0] : val - - if ( - typeof _val === 'string' && - i18n.locales.some((item) => { - if (item.toLowerCase() === _val.toLowerCase()) { - detectedLocale = item - renderOpts.locale = detectedLocale - return true - } - return false - }) - ) { - // remove the locale item from the match - if (isCatchAll) { - ;(val as string[]).splice(0, 1) - } - - // the value is only a locale item and - // shouldn't be added - return isCatchAll ? val.length === 0 : true - } - } - return false - } - - if (routeKeyNames.every((name) => obj[name])) { - return routeKeyNames.reduce((prev, keyName) => { - const paramName = routeKeys?.[keyName] - - if (paramName && !filterLocaleItem(obj[keyName])) { - prev[groups[paramName].pos] = obj[keyName] - } - return prev - }, {} as any) - } - - return Object.keys(obj).reduce((prev, key) => { - if (!filterLocaleItem(obj[key])) { - return Object.assign(prev, { - [key]: obj[key], - }) - } - return prev - }, {}) - }, - }, - groups, - } - })() as any - )(req.headers['x-now-route-matches'] as string) + nowParams = getParamsFromRouteMatches(req, renderOpts, detectedLocale) } // make sure to set renderOpts to the correct params e.g. _params @@ -315,23 +246,10 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { if (pageIsDynamic && nowParams && defaultRouteRegex) { const _parsedUrl = parseUrl(req.url!) - for (const param of Object.keys(defaultRouteRegex.groups)) { - const { optional, repeat } = defaultRouteRegex.groups[param] - let builtParam = `[${repeat ? '...' : ''}${param}]` - - if (optional) { - builtParam = `[${builtParam}]` - } - - const paramIdx = _parsedUrl.pathname!.indexOf(builtParam) - - if (paramIdx > -1) { - _parsedUrl.pathname = - _parsedUrl.pathname!.substr(0, paramIdx) + - encodeURI((nowParams as any)[param] || '') + - _parsedUrl.pathname!.substr(paramIdx + builtParam.length) - } - } + _parsedUrl.pathname = interpolateDynamicPath( + _parsedUrl.pathname!, + nowParams + ) parsedUrl.pathname = _parsedUrl.pathname req.url = formatUrl(_parsedUrl) } diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index 7408c3d0d4159..4eceed9f6d327 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from 'http' import { format as formatUrl, UrlWithParsedQuery } from 'url' -import { ParsedUrlQuery } from 'querystring' +import { parse as parseQs, ParsedUrlQuery } from 'querystring' import { Rewrite } from '../../../../lib/load-custom-routes' import { normalizeLocalePath } from '../../../../next-server/lib/i18n/normalize-locale-path' import pathMatch from '../../../../next-server/lib/router/utils/path-match' @@ -76,7 +76,13 @@ export function getUtils({ basePath, rewrites, pageIsDynamic, -}: ServerlessHandlerCtx) { +}: { + page: ServerlessHandlerCtx['page'] + i18n?: ServerlessHandlerCtx['i18n'] + basePath: ServerlessHandlerCtx['basePath'] + rewrites: ServerlessHandlerCtx['rewrites'] + pageIsDynamic: ServerlessHandlerCtx['pageIsDynamic'] +}) { let defaultRouteRegex: ReturnType | undefined let dynamicRouteMatcher: ReturnType | undefined let defaultRouteMatches: ParsedUrlQuery | undefined @@ -150,6 +156,115 @@ export function getUtils({ parsedUrl.pathname!.replace(new RegExp(`^${basePath}`), '') || '/' } + function getParamsFromRouteMatches( + req: IncomingMessage, + renderOpts?: any, + detectedLocale?: string + ) { + return getRouteMatcher( + (function () { + const { groups, routeKeys } = defaultRouteRegex! + + return { + re: { + // Simulate a RegExp match from the \`req.url\` input + exec: (str: string) => { + const obj = parseQs(str) + + // favor named matches if available + const routeKeyNames = Object.keys(routeKeys || {}) + + const filterLocaleItem = (val: string | string[]) => { + if (i18n) { + // locale items can be included in route-matches + // for fallback SSG pages so ensure they are + // filtered + const isCatchAll = Array.isArray(val) + const _val = isCatchAll ? val[0] : val + + if ( + typeof _val === 'string' && + i18n.locales.some((item) => { + if (item.toLowerCase() === _val.toLowerCase()) { + detectedLocale = item + renderOpts.locale = detectedLocale + return true + } + return false + }) + ) { + // remove the locale item from the match + if (isCatchAll) { + ;(val as string[]).splice(0, 1) + } + + // the value is only a locale item and + // shouldn't be added + return isCatchAll ? val.length === 0 : true + } + } + return false + } + + if (routeKeyNames.every((name) => obj[name])) { + return routeKeyNames.reduce((prev, keyName) => { + const paramName = routeKeys?.[keyName] + + if (paramName && !filterLocaleItem(obj[keyName])) { + prev[groups[paramName].pos] = obj[keyName] + } + return prev + }, {} as any) + } + + return Object.keys(obj).reduce((prev, key) => { + if (!filterLocaleItem(obj[key])) { + return Object.assign(prev, { + [key]: obj[key], + }) + } + return prev + }, {}) + }, + }, + groups, + } + })() as any + )(req.headers['x-now-route-matches'] as string) as ParsedUrlQuery + } + + function interpolateDynamicPath(pathname: string, params: ParsedUrlQuery) { + if (!defaultRouteRegex) return pathname + + for (const param of Object.keys(defaultRouteRegex.groups)) { + const { optional, repeat } = defaultRouteRegex.groups[param] + let builtParam = `[${repeat ? '...' : ''}${param}]` + + if (optional) { + builtParam = `[${builtParam}]` + } + + const paramIdx = pathname!.indexOf(builtParam) + + if (paramIdx > -1) { + let paramValue: string + + if (Array.isArray(params[param])) { + paramValue = (params[param] as string[]).join('/') + } else { + paramValue = params[param] as string + } + + pathname = + pathname.substr(0, paramIdx) + + encodeURI(paramValue || '') + + pathname.substr(paramIdx + builtParam.length) + } + } + + return pathname + } + function normalizeDynamicRouteParams(params: ParsedUrlQuery) { let hasValidParams = true if (!defaultRouteRegex) return { params, hasValidParams } @@ -350,6 +465,8 @@ export function getUtils({ defaultRouteRegex, dynamicRouteMatcher, defaultRouteMatches, + interpolateDynamicPath, + getParamsFromRouteMatches, normalizeDynamicRouteParams, } } diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index dc019aa65dbb7..6c959386351be 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -86,6 +86,7 @@ import { imageOptimizer } from './image-optimizer' import { detectDomainLocale } from '../lib/i18n/detect-domain-locale' import cookie from 'next/dist/compiled/cookie' import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp' +import { getUtils } from '../../build/webpack/loaders/next-serverless-loader/utils' const getCustomRouteMatcher = pathMatch(true) @@ -464,6 +465,62 @@ export default class Server { defaultLocale } + if ( + this.minimalMode && + req.headers['x-matched-path'] && + typeof req.headers['x-matched-path'] === 'string' + ) { + let { pathname, query } = parseUrl( + req.headers['x-matched-path'] as string, + true + ) + let matchedPathname = pathname as string + + // interpolate dynamic params if needed + if (isDynamicRoute(matchedPathname)) { + const utils = getUtils({ + pageIsDynamic: true, + page: matchedPathname, + i18n: this.nextConfig.i18n, + basePath: this.nextConfig.basePath, + rewrites: this.customRoutes.rewrites, + }) + + let params: ParsedUrlQuery | false = {} + const paramsResult = utils.normalizeDynamicRouteParams({ + ...parsedUrl.query, + ...query, + }) + + if (paramsResult.hasValidParams) { + params = paramsResult.params + } else if (req.headers['x-now-route-matches']) { + const opts: Record = {} + params = utils.getParamsFromRouteMatches( + req, + opts, + (parsedUrl.query.__nextLocale as string | undefined) || '' + ) + + if (opts.locale) { + parsedUrl.query.__nextLocale = opts.locale + } + } else { + params = utils.dynamicRouteMatcher!(matchedPathname) + } + + if (params) { + matchedPathname = utils.interpolateDynamicPath( + matchedPathname, + params + ) + } + } + parsedUrl.pathname = `${basePath || ''}${ + parsedUrl.query.__nextLocale || '' + }${matchedPathname}` + } + res.statusCode = 200 try { return await this.run(req, res, parsedUrl) diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index 2cd45aa2dcfca..a655a1fcff836 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -145,4 +145,85 @@ describe('Required Server Files', () => { expect(data4.hello).toBe('world') expect(data4.slug).toBe('third') }) + + it('should render SSR page correctly with x-matched-path', async () => { + const html = await renderViaHTTP(appPort, '/some-other-path', undefined, { + headers: { + 'x-matched-path': '/', + }, + }) + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#index').text()).toBe('index page') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, { + headers: { + 'x-matched-path': '/', + }, + }) + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#index').text()).toBe('index page') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + }) + + it('should render dynamic SSR page correctly with x-matched-path', async () => { + const html = await renderViaHTTP(appPort, '/some-other-path', undefined, { + headers: { + 'x-matched-path': '/dynamic/[slug]?slug=first', + }, + }) + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#dynamic').text()).toBe('dynamic page') + expect($('#slug').text()).toBe('first') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, { + headers: { + 'x-matched-path': '/dynamic/[slug]?slug=second', + }, + }) + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#dynamic').text()).toBe('dynamic page') + expect($2('#slug').text()).toBe('second') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + }) + + it('should render dynamic SSR page correctly with x-matched-path and routes-matches', async () => { + const html = await renderViaHTTP(appPort, '/dynamic/[slug]', undefined, { + headers: { + 'x-matched-path': '/dynamic/[slug]', + 'x-now-route-matches': '1=first', + }, + }) + const $ = cheerio.load(html) + const data = JSON.parse($('#props').text()) + + expect($('#dynamic').text()).toBe('dynamic page') + expect($('#slug').text()).toBe('first') + expect(data.hello).toBe('world') + + const html2 = await renderViaHTTP(appPort, '/dynamic/[slug]', undefined, { + headers: { + 'x-matched-path': '/dynamic/[slug]', + 'x-now-route-matches': '1=second', + }, + }) + const $2 = cheerio.load(html2) + const data2 = JSON.parse($2('#props').text()) + + expect($2('#dynamic').text()).toBe('dynamic page') + expect($2('#slug').text()).toBe('second') + expect(isNaN(data2.random)).toBe(false) + expect(data2.random).not.toBe(data.random) + }) }) From 3a87477a649e56a33c4d63febc1de17846236ace Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 4 Dec 2020 21:26:25 -0600 Subject: [PATCH 4/8] Update test to ensure paths exist --- packages/next/build/index.ts | 18 +++++++++++------- .../font-stylesheet-gathering-plugin.ts | 9 ++++++--- .../required-server-files/test/index.test.js | 5 +++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 2f0ff8d15fba6..31c5e91c5f1d1 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -347,6 +347,12 @@ export default async function build( 'utf8' ) + const manifestPath = path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + PAGES_MANIFEST + ) + const requiredServerFiles = { version: 1, config: { @@ -356,11 +362,14 @@ export default async function build( files: [ ...[ ROUTES_MANIFEST, - PAGES_MANIFEST, + path.relative(distDir, manifestPath), BUILD_MANIFEST, PRERENDER_MANIFEST, REACT_LOADABLE_MANIFEST, - FONT_MANIFEST, + path.join( + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + FONT_MANIFEST + ), BUILD_ID_FILE, ].map((file) => path.join(config.distDir, file)), ], @@ -494,11 +503,6 @@ export default async function build( prefixText: `${Log.prefixes.info} Collecting page data`, }) - const manifestPath = path.join( - distDir, - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - PAGES_MANIFEST - ) const buildManifestPath = path.join(distDir, BUILD_MANIFEST) const ssgPages = new Set() diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts index 3a8763f97b54e..b2c773f78d215 100644 --- a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -7,7 +7,10 @@ import { } from '../../../next-server/server/font-utils' import postcss from 'postcss' import minifier from 'cssnano-simple' -import { OPTIMIZED_FONT_PROVIDERS } from '../../../next-server/lib/constants' +import { + FONT_MANIFEST, + OPTIMIZED_FONT_PROVIDERS, +} from '../../../next-server/lib/constants' // @ts-ignore: TODO: remove ignore when webpack 5 is stable const { RawSource } = webpack.sources || sources @@ -169,7 +172,7 @@ export class FontStylesheetGatheringPlugin { }) } if (!isWebpack5) { - compilation.assets['font-manifest.json'] = new RawSource( + compilation.assets[FONT_MANIFEST] = new RawSource( JSON.stringify(this.manifestContent, null, ' ') ) } @@ -189,7 +192,7 @@ export class FontStylesheetGatheringPlugin { stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets: any) => { - assets['font-manifest.json'] = new RawSource( + assets[FONT_MANIFEST] = new RawSource( JSON.stringify(this.manifestContent, null, ' ') ) } diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index a655a1fcff836..0960d6cb6a0f0 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -75,6 +75,11 @@ describe('Required Server Files', () => { expect(requiredFilesManifest.files.length).toBeGreaterThan(0) expect(requiredFilesManifest.ignore.length).toBeGreaterThan(0) expect(typeof requiredFilesManifest.config.trailingSlash).toBe('boolean') + + for (const file of requiredFilesManifest.files) { + console.log('checking', file) + expect(await fs.exists(join(appDir, file))).toBe(true) + } }) it('should render SSR page correctly', async () => { From ebda95df545547e4fc5fe0350feee276b826ac8f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Sat, 5 Dec 2020 12:57:55 -0600 Subject: [PATCH 5/8] Omit config file path --- packages/next/build/index.ts | 1 + test/integration/required-server-files/test/index.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 31c5e91c5f1d1..251d24f6edb0b 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -358,6 +358,7 @@ export default async function build( config: { ...config, compress: false, + configFile: undefined, }, files: [ ...[ diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index 0960d6cb6a0f0..2bec78a4ab8cb 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -74,6 +74,7 @@ describe('Required Server Files', () => { expect(Array.isArray(requiredFilesManifest.ignore)).toBe(true) expect(requiredFilesManifest.files.length).toBeGreaterThan(0) expect(requiredFilesManifest.ignore.length).toBeGreaterThan(0) + expect(typeof requiredFilesManifest.config.configFile).toBe('undefined') expect(typeof requiredFilesManifest.config.trailingSlash).toBe('boolean') for (const file of requiredFilesManifest.files) { From 467e951faa4983b8e80e9c6a0dac1f4fede668bd Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Sat, 5 Dec 2020 14:40:34 -0600 Subject: [PATCH 6/8] Disable redirects in minimal mode --- .../next/next-server/server/next-server.ts | 66 ++++++++++--------- .../required-server-files/test/index.test.js | 25 ++++++- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 5edabccfb1198..14d9eb9c1bb5b 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -777,44 +777,46 @@ export default class Server { }) } - const redirects = this.customRoutes.redirects.map((redirect) => { - const redirectRoute = getCustomRoute(redirect, 'redirect') - return { - type: redirectRoute.type, - match: redirectRoute.match, - statusCode: redirectRoute.statusCode, - name: `Redirect route ${redirectRoute.source}`, - fn: async (req, res, params, parsedUrl) => { - const { parsedDestination } = prepareDestination( - redirectRoute.destination, - params, - parsedUrl.query, - false - ) + const redirects = this.minimalMode + ? [] + : this.customRoutes.redirects.map((redirect) => { + const redirectRoute = getCustomRoute(redirect, 'redirect') + return { + type: redirectRoute.type, + match: redirectRoute.match, + statusCode: redirectRoute.statusCode, + name: `Redirect route ${redirectRoute.source}`, + fn: async (req, res, params, parsedUrl) => { + const { parsedDestination } = prepareDestination( + redirectRoute.destination, + params, + parsedUrl.query, + false + ) - const { query } = parsedDestination - delete (parsedDestination as any).query + const { query } = parsedDestination + delete (parsedDestination as any).query - parsedDestination.search = stringifyQuery(req, query) + parsedDestination.search = stringifyQuery(req, query) - const updatedDestination = formatUrl(parsedDestination) + const updatedDestination = formatUrl(parsedDestination) - res.setHeader('Location', updatedDestination) - res.statusCode = getRedirectStatus(redirectRoute as Redirect) + res.setHeader('Location', updatedDestination) + res.statusCode = getRedirectStatus(redirectRoute as Redirect) - // Since IE11 doesn't support the 308 header add backwards - // compatibility using refresh header - if (res.statusCode === 308) { - res.setHeader('Refresh', `0;url=${updatedDestination}`) - } + // Since IE11 doesn't support the 308 header add backwards + // compatibility using refresh header + if (res.statusCode === 308) { + res.setHeader('Refresh', `0;url=${updatedDestination}`) + } - res.end() - return { - finished: true, - } - }, - } as Route - }) + res.end() + return { + finished: true, + } + }, + } as Route + }) const rewrites = this.customRoutes.rewrites.map((rewrite) => { const rewriteRoute = getCustomRoute(rewrite, 'rewrite') diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index 2bec78a4ab8cb..4c3d62458f947 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -5,7 +5,12 @@ import fs from 'fs-extra' import { join } from 'path' import cheerio from 'cheerio' import Server from 'next/dist/next-server/server/next-server' -import { findPort, nextBuild, renderViaHTTP } from 'next-test-utils' +import { + fetchViaHTTP, + findPort, + nextBuild, + renderViaHTTP, +} from 'next-test-utils' jest.setTimeout(1000 * 60 * 2) @@ -232,4 +237,22 @@ describe('Required Server Files', () => { expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) + + it('should not apply trailingSlash redirect', async () => { + for (const path of [ + '/', + '/dynamic/another/', + '/dynamic/another', + '/fallback/first/', + '/fallback/first', + '/fallback/another/', + '/fallback/another', + ]) { + const res = await fetchViaHTTP(appPort, path, undefined, { + redirect: 'manual', + }) + + expect(res.status).toBe(200) + } + }) }) From d1d6849ef396b33db59a23da4fc90ee0d1da7c3f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 10 Dec 2020 16:16:42 -0600 Subject: [PATCH 7/8] Ensure _next/data resolves against matched path --- .../next-serverless-loader/page-handler.ts | 13 +---- .../loaders/next-serverless-loader/utils.ts | 17 ++++++- .../next/next-server/server/next-server.ts | 32 ++++++++++-- .../pages/dynamic/[slug].js | 4 ++ .../pages/fallback/[slug].js | 4 ++ .../required-server-files/pages/index.js | 6 ++- .../required-server-files/test/index.test.js | 49 ++++++++++++++++--- 7 files changed, 100 insertions(+), 25 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 0c4575dbcb47f..c7e5b15568b5c 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -57,6 +57,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { interpolateDynamicPath, getParamsFromRouteMatches, normalizeDynamicRouteParams, + normalizeVercelUrl, } = getUtils(ctx) async function renderReqToHTML( @@ -229,17 +230,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { // if provided from worker or params if we're parsing them here renderOpts.params = _params || params - // make sure to normalize req.url on Vercel to strip dynamic params - // from the query which are added during routing - if (pageIsDynamic && trustQuery && defaultRouteRegex) { - const _parsedUrl = parseUrl(req.url!, true) - delete (_parsedUrl as any).search - - for (const param of Object.keys(defaultRouteRegex.groups)) { - delete _parsedUrl.query[param] - } - req.url = formatUrl(_parsedUrl) - } + normalizeVercelUrl(req, !!trustQuery) // normalize request URL/asPath for fallback/revalidate pages since the // proxy sets the request URL to the output's path for fallback pages diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index b39a61c439e95..444487707dcfd 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -1,5 +1,5 @@ import { IncomingMessage, ServerResponse } from 'http' -import { format as formatUrl, UrlWithParsedQuery } from 'url' +import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url' import { parse as parseQs, ParsedUrlQuery } from 'querystring' import { Rewrite } from '../../../../lib/load-custom-routes' import { normalizeLocalePath } from '../../../../next-server/lib/i18n/normalize-locale-path' @@ -256,6 +256,20 @@ export function getUtils({ return pathname } + function normalizeVercelUrl(req: IncomingMessage, trustQuery: boolean) { + // make sure to normalize req.url on Vercel to strip dynamic params + // from the query which are added during routing + if (pageIsDynamic && trustQuery && defaultRouteRegex) { + const _parsedUrl = parseUrl(req.url!, true) + delete (_parsedUrl as any).search + + for (const param of Object.keys(defaultRouteRegex.groups)) { + delete _parsedUrl.query[param] + } + req.url = formatUrl(_parsedUrl) + } + } + function normalizeDynamicRouteParams(params: ParsedUrlQuery) { let hasValidParams = true if (!defaultRouteRegex) return { params, hasValidParams } @@ -454,6 +468,7 @@ export function getUtils({ handleRewrites, handleBasePath, defaultRouteRegex, + normalizeVercelUrl, dynamicRouteMatcher, defaultRouteMatches, interpolateDynamicPath, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index ae66ff00daa3c..6e82930e87059 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -460,17 +460,28 @@ export default class Server { req.headers['x-matched-path'] && typeof req.headers['x-matched-path'] === 'string' ) { - let { pathname, query } = parseUrl( - req.headers['x-matched-path'] as string, + const reqUrlIsDataUrl = req.url?.includes('/_next/data') + const matchedPathIsDataUrl = req.headers['x-matched-path']?.includes( + '/_next/data' + ) + const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl + + let parsedPath = parseUrl( + isDataUrl ? req.url! : (req.headers['x-matched-path'] as string), true ) + const { pathname, query } = parsedPath let matchedPathname = pathname as string - // interpolate dynamic params if needed - if (isDynamicRoute(matchedPathname)) { + const matchedPathnameNoExt = isDataUrl + ? matchedPathname.replace(/\.json$/, '') + : matchedPathname + + // interpolate dynamic params and normalize URL if needed + if (isDynamicRoute(matchedPathnameNoExt)) { const utils = getUtils({ pageIsDynamic: true, - page: matchedPathname, + page: matchedPathnameNoExt, i18n: this.nextConfig.i18n, basePath: this.nextConfig.basePath, rewrites: this.customRoutes.rewrites, @@ -504,7 +515,18 @@ export default class Server { matchedPathname, params ) + + req.url = utils.interpolateDynamicPath(req.url!, params) + } + + if (reqUrlIsDataUrl && matchedPathIsDataUrl) { + req.url = formatUrl({ + ...parsedPath, + pathname: matchedPathname, + }) } + Object.assign(parsedUrl.query, params) + utils.normalizeVercelUrl(req, true) } parsedUrl.pathname = `${basePath || ''}${ parsedUrl.query.__nextLocale || '' diff --git a/test/integration/required-server-files/pages/dynamic/[slug].js b/test/integration/required-server-files/pages/dynamic/[slug].js index 70e434ce330c4..b5a19a314ad08 100644 --- a/test/integration/required-server-files/pages/dynamic/[slug].js +++ b/test/integration/required-server-files/pages/dynamic/[slug].js @@ -1,3 +1,5 @@ +import { useRouter } from 'next/router' + export const getServerSideProps = ({ params }) => { return { props: { @@ -9,10 +11,12 @@ export const getServerSideProps = ({ params }) => { } export default function Page(props) { + const router = useRouter() return ( <>

dynamic page

{props.slug}

+

{JSON.stringify(router)}

{JSON.stringify(props)}

) diff --git a/test/integration/required-server-files/pages/fallback/[slug].js b/test/integration/required-server-files/pages/fallback/[slug].js index 6cd7a0c19470e..518e2e10cafc6 100644 --- a/test/integration/required-server-files/pages/fallback/[slug].js +++ b/test/integration/required-server-files/pages/fallback/[slug].js @@ -1,3 +1,5 @@ +import { useRouter } from 'next/router' + export const getStaticProps = ({ params }) => { return { props: { @@ -16,10 +18,12 @@ export const getStaticPaths = () => { } export default function Page(props) { + const router = useRouter() return ( <>

fallback page

{props.slug}

+

{JSON.stringify(router)}

{JSON.stringify(props)}

) diff --git a/test/integration/required-server-files/pages/index.js b/test/integration/required-server-files/pages/index.js index 29115ad174773..5d287f08b6681 100644 --- a/test/integration/required-server-files/pages/index.js +++ b/test/integration/required-server-files/pages/index.js @@ -1,4 +1,6 @@ -export const getServerSideProps = () => { +import { useRouter } from 'next/router' + +export const getServerSideProps = ({ req }) => { return { props: { hello: 'world', @@ -8,9 +10,11 @@ export const getServerSideProps = () => { } export default function Page(props) { + const router = useRouter() return ( <>

index page

+

{JSON.stringify(router)}

{JSON.stringify(props)}

) diff --git a/test/integration/required-server-files/test/index.test.js b/test/integration/required-server-files/test/index.test.js index 4c3d62458f947..4ea34ab10b8fb 100644 --- a/test/integration/required-server-files/test/index.test.js +++ b/test/integration/required-server-files/test/index.test.js @@ -209,35 +209,70 @@ describe('Required Server Files', () => { expect(data2.random).not.toBe(data.random) }) - it('should render dynamic SSR page correctly with x-matched-path and routes-matches', async () => { - const html = await renderViaHTTP(appPort, '/dynamic/[slug]', undefined, { + it('should render fallback page correctly with x-matched-path and routes-matches', async () => { + const html = await renderViaHTTP(appPort, '/fallback/first', undefined, { headers: { - 'x-matched-path': '/dynamic/[slug]', + 'x-matched-path': '/fallback/first', 'x-now-route-matches': '1=first', }, }) const $ = cheerio.load(html) const data = JSON.parse($('#props').text()) - expect($('#dynamic').text()).toBe('dynamic page') + expect($('#fallback').text()).toBe('fallback page') expect($('#slug').text()).toBe('first') expect(data.hello).toBe('world') - const html2 = await renderViaHTTP(appPort, '/dynamic/[slug]', undefined, { + const html2 = await renderViaHTTP(appPort, `/fallback/[slug]`, undefined, { headers: { - 'x-matched-path': '/dynamic/[slug]', + 'x-matched-path': '/fallback/[slug]', 'x-now-route-matches': '1=second', }, }) const $2 = cheerio.load(html2) const data2 = JSON.parse($2('#props').text()) - expect($2('#dynamic').text()).toBe('dynamic page') + expect($2('#fallback').text()).toBe('fallback page') expect($2('#slug').text()).toBe('second') expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) + it('should return data correctly with x-matched-path', async () => { + const res = await fetchViaHTTP( + appPort, + `/_next/data/${buildId}/dynamic/first.json`, + undefined, + { + headers: { + 'x-matched-path': '/dynamic/[slug]?slug=first', + }, + } + ) + + const { pageProps: data } = await res.json() + + expect(data.slug).toBe('first') + expect(data.hello).toBe('world') + + const res2 = await fetchViaHTTP( + appPort, + `/_next/data/${buildId}/fallback/[slug].json`, + undefined, + { + headers: { + 'x-matched-path': `/_next/data/${buildId}/fallback/[slug].json`, + 'x-now-route-matches': '1=second', + }, + } + ) + + const { pageProps: data2 } = await res2.json() + + expect(data2.slug).toBe('second') + expect(data2.hello).toBe('world') + }) + it('should not apply trailingSlash redirect', async () => { for (const path of [ '/', From 1d6083ee5b3715f2094f5e64b0477b5438bf9987 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 16 Dec 2020 14:21:48 -0600 Subject: [PATCH 8/8] Remove extra array spread --- packages/next/build/index.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 251d24f6edb0b..a79aeb8ae3960 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -361,19 +361,17 @@ export default async function build( configFile: undefined, }, files: [ - ...[ - ROUTES_MANIFEST, - path.relative(distDir, manifestPath), - BUILD_MANIFEST, - PRERENDER_MANIFEST, - REACT_LOADABLE_MANIFEST, - path.join( - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - FONT_MANIFEST - ), - BUILD_ID_FILE, - ].map((file) => path.join(config.distDir, file)), - ], + ROUTES_MANIFEST, + path.relative(distDir, manifestPath), + BUILD_MANIFEST, + PRERENDER_MANIFEST, + REACT_LOADABLE_MANIFEST, + path.join( + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + FONT_MANIFEST + ), + BUILD_ID_FILE, + ].map((file) => path.join(config.distDir, file)), ignore: [ path.relative( dir,