From bd188223567012e1a03a2066d7e6c6b6372b0a43 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 11 Aug 2022 01:06:43 +0200 Subject: [PATCH 1/4] remove path polyfill from edge server --- .../shared/lib/page-path/normalize-page-path.ts | 14 ++++++++------ test/e2e/switchable-runtime/next.config.js | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/next/shared/lib/page-path/normalize-page-path.ts b/packages/next/shared/lib/page-path/normalize-page-path.ts index 81e7383b2da4019..d913615d5126633 100644 --- a/packages/next/shared/lib/page-path/normalize-page-path.ts +++ b/packages/next/shared/lib/page-path/normalize-page-path.ts @@ -1,6 +1,5 @@ import { ensureLeadingSlash } from './ensure-leading-slash' import { isDynamicRoute } from '../router/utils' -import { posix } from '../isomorphic/path' import { NormalizeError } from '../utils' /** @@ -21,11 +20,14 @@ export function normalizePagePath(page: string): string { : page ) - const resolvedPage = posix.normalize(normalized) - if (resolvedPage !== normalized) { - throw new NormalizeError( - `Requested and resolved page mismatch: ${normalized} ${resolvedPage}` - ) + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { posix } = require('path') + const resolvedPage = posix.normalize(normalized) + if (resolvedPage !== normalized) { + throw new NormalizeError( + `Requested and resolved page mismatch: ${normalized} ${resolvedPage}` + ) + } } return normalized diff --git a/test/e2e/switchable-runtime/next.config.js b/test/e2e/switchable-runtime/next.config.js index 31dd1d92233eba0..1c3c436e114ed78 100644 --- a/test/e2e/switchable-runtime/next.config.js +++ b/test/e2e/switchable-runtime/next.config.js @@ -2,6 +2,5 @@ module.exports = { reactStrictMode: true, experimental: { appDir: true, - serverComponents: true, }, } From 8f557ee7f84e55af28f2a1676cd3d8e33f726406 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 12 Aug 2022 15:33:12 +0200 Subject: [PATCH 2/4] move incremental-cache and path out of base-server --- packages/next/build/webpack-config.ts | 6 +- packages/next/lib/constants.ts | 8 -- packages/next/server/base-server.ts | 61 +++------ packages/next/server/next-server.ts | 37 ++++- .../index.ts} | 85 ++---------- packages/next/server/response-cache/types.ts | 90 ++++++++++++ packages/next/server/response-cache/web.ts | 129 ++++++++++++++++++ packages/next/server/web-server.ts | 7 + 8 files changed, 294 insertions(+), 129 deletions(-) rename packages/next/server/{response-cache.ts => response-cache/index.ts} (76%) create mode 100644 packages/next/server/response-cache/types.ts create mode 100644 packages/next/server/response-cache/web.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 9dbcf2679778d54..8d89c4085e519f0 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -7,8 +7,6 @@ import path, { dirname, join as pathJoin, relative as relativePath } from 'path' import { escapeStringRegexp } from '../shared/lib/escape-regexp' import { DOT_NEXT_ALIAS, - NEXT_PROJECT_ROOT, - NEXT_PROJECT_ROOT_DIST_CLIENT, PAGES_DIR_ALIAS, ROOT_DIR_ALIAS, APP_DIR_ALIAS, @@ -64,6 +62,10 @@ import { loadBindings } from './swc' import { clientComponentRegex } from './webpack/loaders/utils' import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plugin' +const NEXT_PROJECT_ROOT = pathJoin(__dirname, '..', '..') +const NEXT_PROJECT_ROOT_DIST = pathJoin(NEXT_PROJECT_ROOT, 'dist') +const NEXT_PROJECT_ROOT_DIST_CLIENT = pathJoin(NEXT_PROJECT_ROOT_DIST, 'client') + const watchOptions = Object.freeze({ aggregateTimeout: 5, ignored: ['**/.git/**', '**/.next/**'], diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index ae0fc0cdd87a151..f4efa6bfbefdc51 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -1,12 +1,4 @@ import type { ServerRuntime } from '../types' -import { join } from '../shared/lib/isomorphic/path' - -export const NEXT_PROJECT_ROOT = join(__dirname, '..', '..') -export const NEXT_PROJECT_ROOT_DIST = join(NEXT_PROJECT_ROOT, 'dist') -export const NEXT_PROJECT_ROOT_DIST_CLIENT = join( - NEXT_PROJECT_ROOT_DIST, - 'client' -) // Regex for API routes export const API_ROUTE = /^\/api(?:\/|$)/ diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index ee2003f679139ec..def23f795c212bc 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -10,7 +10,11 @@ import type { NextConfig, NextConfigComplete } from './config-shared' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { ParsedUrlQuery } from 'querystring' import type { RenderOpts, RenderOptsPartial } from './render' -import type { ResponseCacheEntry, ResponseCacheValue } from './response-cache' +import type { + ResponseCacheBase, + ResponseCacheEntry, + ResponseCacheValue, +} from './response-cache' import type { UrlWithParsedQuery } from 'url' import { CacheFs, @@ -24,7 +28,7 @@ import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plug import type { BaseNextRequest, BaseNextResponse } from './base-http' import type { PayloadOptions } from './send-payload' -import { join, resolve } from '../shared/lib/isomorphic/path' +import { join } from '../shared/lib/isomorphic/path' import { parse as parseQs } from 'querystring' import { format as formatUrl, parse as parseUrl } from 'url' import { getRedirectStatus } from '../lib/redirect-status' @@ -46,7 +50,6 @@ import { isTargetLikeServerless } from './utils' import Router from './router' import { getPathMatch } from '../shared/lib/router/utils/path-match' import { setRevalidateHeaders } from './send-payload/revalidate-headers' -import { IncrementalCache } from './lib/incremental-cache' import { execOnce } from '../shared/lib/utils' import { isBlockedPage, isBot } from './utils' import RenderResult from './render-result' @@ -58,7 +61,6 @@ import * as Log from '../build/output/log' import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale' import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' import { getUtils } from '../build/webpack/loaders/next-serverless-loader/utils' -import ResponseCache from './response-cache' import isError, { getProperError } from '../lib/is-error' import { addRequestMeta, getRequestMeta } from './request-meta' import { createHeaderRoute, createRedirectRoute } from './server-route-utils' @@ -72,7 +74,6 @@ import { getLocaleRedirect } from '../shared/lib/i18n/get-locale-redirect' import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' -import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -203,8 +204,7 @@ export default abstract class Server { largePageDataBytes?: number } protected serverOptions: ServerOptions - private incrementalCache: IncrementalCache - private responseCache: ResponseCache + private responseCache: ResponseCacheBase protected router: Router protected dynamicRoutes?: DynamicRoutes protected appPathRoutes?: Record @@ -250,6 +250,7 @@ export default abstract class Server { req: BaseNextRequest, parsedUrl: NextUrlWithParsedQuery ): void + protected abstract getFallback(page: string): Promise protected abstract sendRenderResult( req: BaseNextRequest, @@ -285,6 +286,10 @@ export default abstract class Server { res: BaseNextResponse ): void + protected abstract getResponseCache(options: { + dev: boolean + }): ResponseCacheBase + protected abstract loadEnvConfig(params: { dev: boolean forceReload?: boolean @@ -303,7 +308,9 @@ export default abstract class Server { } = options this.serverOptions = options - this.dir = resolve(dir) + this.dir = + process.env.NEXT_RUNTIME === 'edge' ? dir : require('path').resolve(dir) + this.quiet = quiet this.loadEnvConfig({ dev }) @@ -312,7 +319,10 @@ export default abstract class Server { this.nextConfig = conf as NextConfigComplete this.hostname = hostname this.port = port - this.distDir = join(this.dir, this.nextConfig.distDir) + this.distDir = + process.env.NEXT_RUNTIME === 'edge' + ? this.nextConfig.distDir + : require('path').join(this.dir, this.nextConfig.distDir) this.publicDir = this.getPublicDir() this.hasStaticDir = !minimalMode && this.getHasStaticDir() @@ -385,32 +395,7 @@ export default abstract class Server { this.router = new Router(this.generateRoutes()) this.setAssetPrefix(assetPrefix) - this.incrementalCache = new IncrementalCache({ - fs: this.getCacheFilesystem(), - dev, - serverDistDir: this.serverDistDir, - maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize, - flushToDisk: !minimalMode && this.nextConfig.experimental.isrFlushToDisk, - incrementalCacheHandlerPath: - this.nextConfig.experimental?.incrementalCacheHandlerPath, - getPrerenderManifest: () => { - if (dev) { - return { - version: -1 as any, // letting us know this doesn't conform to spec - routes: {}, - dynamicRoutes: {}, - notFoundRoutes: [], - preview: null as any, // `preview` is special case read in next-dev-server - } - } else { - return this.getPrerenderManifest() - } - }, - }) - this.responseCache = new ResponseCache( - this.incrementalCache, - this.minimalMode - ) + this.responseCache = this.getResponseCache({ dev }) } public logError(err: Error): void { @@ -730,12 +715,6 @@ export default abstract class Server { return Object.assign(customRoutes, { rewrites }) } - protected getFallback(page: string): Promise { - page = normalizePagePath(page) - const cacheFs = this.getCacheFilesystem() - return cacheFs.readFile(join(this.serverDistDir, 'pages', `${page}.html`)) - } - protected getPreviewProps(): __ApiPreviewProps { return this.getPrerenderManifest().preview } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 2148b9d96f3574c..0eb6747391f5c03 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -80,13 +80,14 @@ import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' import { loadEnvConfig } from '@next/env' import { getCustomRoute } from './server-route-utils' import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring' -import ResponseCache from '../server/response-cache' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' import { bodyStreamToNodeStream, getClonableBody } from './body-streams' import { checkIsManualRevalidate } from './api-utils' import { isDynamicRoute } from '../shared/lib/router/utils' import { shouldUseReactRoot } from './utils' +import ResponseCache from './response-cache' +import { IncrementalCache } from './lib/incremental-cache' if (shouldUseReactRoot) { ;(process.env as any).__NEXT_REACT_ROOT = 'true' @@ -171,6 +172,34 @@ export default class NextNodeServer extends BaseServer { loadEnvConfig(this.dir, dev, Log, forceReload) } + protected getResponseCache({ dev }: { dev: boolean }) { + const incrementalCache = new IncrementalCache({ + fs: this.getCacheFilesystem(), + dev, + serverDistDir: this.serverDistDir, + maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize, + flushToDisk: + !this.minimalMode && this.nextConfig.experimental.isrFlushToDisk, + incrementalCacheHandlerPath: + this.nextConfig.experimental?.incrementalCacheHandlerPath, + getPrerenderManifest: () => { + if (dev) { + return { + version: -1 as any, // letting us know this doesn't conform to spec + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: null as any, // `preview` is special case read in next-dev-server + } + } else { + return this.getPrerenderManifest() + } + }, + }) + + return new ResponseCache(incrementalCache, this.minimalMode) + } + protected getPublicDir(): string { return join(this.dir, CLIENT_PUBLIC_FILES_PATH) } @@ -798,6 +827,12 @@ export default class NextNodeServer extends BaseServer { )) } + protected getFallback(page: string): Promise { + page = normalizePagePath(page) + const cacheFs = this.getCacheFilesystem() + return cacheFs.readFile(join(this.serverDistDir, 'pages', `${page}.html`)) + } + protected getCacheFilesystem(): CacheFs { return { readFile: (f) => fs.promises.readFile(f, 'utf8'), diff --git a/packages/next/server/response-cache.ts b/packages/next/server/response-cache/index.ts similarity index 76% rename from packages/next/server/response-cache.ts rename to packages/next/server/response-cache/index.ts index b2acbbdbf8be973..d6a918955ee1986 100644 --- a/packages/next/server/response-cache.ts +++ b/packages/next/server/response-cache/index.ts @@ -1,82 +1,13 @@ -import RenderResult from './render-result' +import type { + IncrementalCache, + ResponseCacheEntry, + ResponseGenerator, + IncrementalCacheItem, +} from './types' -export interface CachedRedirectValue { - kind: 'REDIRECT' - props: Object -} - -interface CachedPageValue { - kind: 'PAGE' - // this needs to be a RenderResult so since renderResponse - // expects that type instead of a string - html: RenderResult - pageData: Object -} - -export interface CachedImageValue { - kind: 'IMAGE' - etag: string - buffer: Buffer - extension: string - isMiss?: boolean - isStale?: boolean -} - -interface IncrementalCachedPageValue { - kind: 'PAGE' - // this needs to be a string since the cache expects to store - // the string value - html: string - pageData: Object -} - -export type IncrementalCacheEntry = { - curRevalidate?: number | false - // milliseconds to revalidate after - revalidateAfter: number | false - isStale?: boolean - value: IncrementalCacheValue | null -} - -export type IncrementalCacheValue = - | CachedRedirectValue - | IncrementalCachedPageValue - | CachedImageValue - -export type ResponseCacheValue = - | CachedRedirectValue - | CachedPageValue - | CachedImageValue - -export type ResponseCacheEntry = { - revalidate?: number | false - value: ResponseCacheValue | null - isStale?: boolean - isMiss?: boolean -} - -type ResponseGenerator = ( - hasResolved: boolean, - hadCache: boolean -) => Promise +import RenderResult from '../render-result' -type IncrementalCacheItem = { - revalidateAfter?: number | false - curRevalidate?: number | false - revalidate?: number | false - value: IncrementalCacheValue | null - isStale?: boolean - isMiss?: boolean -} | null - -interface IncrementalCache { - get: (key: string) => Promise - set: ( - key: string, - data: IncrementalCacheValue | null, - revalidate?: number | false - ) => Promise -} +export * from './types' export default class ResponseCache { incrementalCache: IncrementalCache diff --git a/packages/next/server/response-cache/types.ts b/packages/next/server/response-cache/types.ts new file mode 100644 index 000000000000000..63c2d37c4f6598d --- /dev/null +++ b/packages/next/server/response-cache/types.ts @@ -0,0 +1,90 @@ +import type RenderResult from '../render-result' + +export interface ResponseCacheBase { + get( + key: string | null, + responseGenerator: ResponseGenerator, + context: { + isManualRevalidate?: boolean + isPrefetch?: boolean + } + ): Promise +} + +export interface CachedRedirectValue { + kind: 'REDIRECT' + props: Object +} + +interface CachedPageValue { + kind: 'PAGE' + // this needs to be a RenderResult so since renderResponse + // expects that type instead of a string + html: RenderResult + pageData: Object +} + +export interface CachedImageValue { + kind: 'IMAGE' + etag: string + buffer: Buffer + extension: string + isMiss?: boolean + isStale?: boolean +} + +interface IncrementalCachedPageValue { + kind: 'PAGE' + // this needs to be a string since the cache expects to store + // the string value + html: string + pageData: Object +} + +export type IncrementalCacheEntry = { + curRevalidate?: number | false + // milliseconds to revalidate after + revalidateAfter: number | false + isStale?: boolean + value: IncrementalCacheValue | null +} + +export type IncrementalCacheValue = + | CachedRedirectValue + | IncrementalCachedPageValue + | CachedImageValue + +export type ResponseCacheValue = + | CachedRedirectValue + | CachedPageValue + | CachedImageValue + +export type ResponseCacheEntry = { + revalidate?: number | false + value: ResponseCacheValue | null + isStale?: boolean + isMiss?: boolean +} + +export type ResponseGenerator = ( + hasResolved: boolean, + hadCache: boolean +) => Promise + +export type IncrementalCacheItem = { + revalidateAfter?: number | false + curRevalidate?: number | false + revalidate?: number | false + value: IncrementalCacheValue | null + isStale?: boolean + isMiss?: boolean +} | null + +export interface IncrementalCache { + get: (key: string) => Promise + set: ( + key: string, + data: IncrementalCacheValue | null, + revalidate?: number | false + ) => Promise +} diff --git a/packages/next/server/response-cache/web.ts b/packages/next/server/response-cache/web.ts new file mode 100644 index 000000000000000..619b0bd81f18a9d --- /dev/null +++ b/packages/next/server/response-cache/web.ts @@ -0,0 +1,129 @@ +import type { ResponseCacheEntry, ResponseGenerator } from './types' + +/** + * In the web server, there is currently no incremental cache provided and we + * always SSR the page. + */ +export default class WebResponseCache { + pendingResponses: Map> + previousCacheItem?: { + key: string + entry: ResponseCacheEntry | null + expiresAt: number + } + minimalMode?: boolean + + constructor(minimalMode: boolean) { + this.pendingResponses = new Map() + this.minimalMode = minimalMode + } + + public get( + key: string | null, + responseGenerator: ResponseGenerator, + context: { + isManualRevalidate?: boolean + isPrefetch?: boolean + } + ): Promise { + // ensure manual revalidate doesn't block normal requests + const pendingResponseKey = key + ? `${key}-${context.isManualRevalidate ? '1' : '0'}` + : null + + const pendingResponse = pendingResponseKey + ? this.pendingResponses.get(pendingResponseKey) + : null + if (pendingResponse) { + return pendingResponse + } + + let resolver: (cacheEntry: ResponseCacheEntry | null) => void = () => {} + let rejecter: (error: Error) => void = () => {} + const promise: Promise = new Promise( + (resolve, reject) => { + resolver = resolve + rejecter = reject + } + ) + if (pendingResponseKey) { + this.pendingResponses.set(pendingResponseKey, promise) + } + + let resolved = false + const resolve = (cacheEntry: ResponseCacheEntry | null) => { + if (pendingResponseKey) { + // Ensure all reads from the cache get the latest value. + this.pendingResponses.set( + pendingResponseKey, + Promise.resolve(cacheEntry) + ) + } + if (!resolved) { + resolved = true + resolver(cacheEntry) + } + } + + // we keep the previous cache entry around to leverage + // when the incremental cache is disabled in minimal mode + if ( + pendingResponseKey && + this.minimalMode && + this.previousCacheItem?.key === pendingResponseKey && + this.previousCacheItem.expiresAt > Date.now() + ) { + resolve(this.previousCacheItem.entry) + this.pendingResponses.delete(pendingResponseKey) + return promise + } + + // We wait to do any async work until after we've added our promise to + // `pendingResponses` to ensure that any any other calls will reuse the + // same promise until we've fully finished our work. + ;(async () => { + try { + const cacheEntry = await responseGenerator(resolved, false) + const resolveValue = + cacheEntry === null + ? null + : { + ...cacheEntry, + isMiss: true, + } + + // for manual revalidate wait to resolve until cache is set + if (!context.isManualRevalidate) { + resolve(resolveValue) + } + + if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { + this.previousCacheItem = { + key: pendingResponseKey || key, + entry: cacheEntry, + expiresAt: Date.now() + 1000, + } + } else { + this.previousCacheItem = undefined + } + + if (context.isManualRevalidate) { + resolve(resolveValue) + } + } catch (err) { + // while revalidating in the background we can't reject as + // we already resolved the cache entry so log the error here + if (resolved) { + console.error(err) + } else { + rejecter(err as Error) + } + } finally { + if (pendingResponseKey) { + this.pendingResponses.delete(pendingResponseKey) + } + } + })() + return promise + } +} diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 4970c4a9a982b35..f6020a851cafa3f 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -12,6 +12,7 @@ import { renderToHTML } from './render' import { byteLength } from './api-utils/web' import { generateETag } from './lib/etag' import { addRequestMeta } from './request-meta' +import WebResponseCache from './response-cache/web' interface WebServerOptions extends Options { webServerConfig: { @@ -44,6 +45,9 @@ export default class NextWebServer extends BaseServer { // For the web server layer, compression is automatically handled by the // upstream proxy (edge runtime or node server) and we can simply skip here. } + protected getResponseCache() { + return new WebResponseCache(this.minimalMode) + } protected getRoutesManifest() { return { headers: [], @@ -73,6 +77,9 @@ export default class NextWebServer extends BaseServer { protected getHasStaticDir() { return false } + protected async getFallback() { + return '' + } protected generateImageRoutes() { return [] } From 03413644bfb6d618bceb305a5a6f80a1c4333815 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 12 Aug 2022 16:43:25 +0200 Subject: [PATCH 3/4] fix test --- test/e2e/switchable-runtime/next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/switchable-runtime/next.config.js b/test/e2e/switchable-runtime/next.config.js index 1c3c436e114ed78..31dd1d92233eba0 100644 --- a/test/e2e/switchable-runtime/next.config.js +++ b/test/e2e/switchable-runtime/next.config.js @@ -2,5 +2,6 @@ module.exports = { reactStrictMode: true, experimental: { appDir: true, + serverComponents: true, }, } From 35c4b13a4cf58b6713db646f39599eb4be8147ec Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 12 Aug 2022 17:03:02 +0200 Subject: [PATCH 4/4] fix condition --- packages/next/shared/lib/page-path/normalize-page-path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/shared/lib/page-path/normalize-page-path.ts b/packages/next/shared/lib/page-path/normalize-page-path.ts index d913615d5126633..54db8eb6047be43 100644 --- a/packages/next/shared/lib/page-path/normalize-page-path.ts +++ b/packages/next/shared/lib/page-path/normalize-page-path.ts @@ -20,7 +20,7 @@ export function normalizePagePath(page: string): string { : page ) - if (process.env.NEXT_RUNTIME === 'nodejs') { + if (process.env.NEXT_RUNTIME !== 'edge') { const { posix } = require('path') const resolvedPage = posix.normalize(normalized) if (resolvedPage !== normalized) {