From ca8fd316f1091f9af4564e4286ccdfdaaf43d336 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 13 Dec 2021 07:11:12 -0600 Subject: [PATCH 1/5] Update docker image to leverage output traces (#32258) This updates our docker example to leverage the output traces and standalone build to reduce the resulting docker image quite a bit. docker image size before: `272MB` docker image size after: `121MB` node-14:alpine size (base image): `118MB` ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint` x-ref: https://github.com/vercel/next.js/pull/32255 x-ref: https://github.com/vercel/next.js/issues/32252 x-ref: https://github.com/vercel/next.js/issues/30822 --- examples/with-docker/Dockerfile | 9 ++++++--- examples/with-docker/next.config.js | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 examples/with-docker/next.config.js diff --git a/examples/with-docker/Dockerfile b/examples/with-docker/Dockerfile index 1045f94cbe2d8..461c289d0b9d3 100644 --- a/examples/with-docker/Dockerfile +++ b/examples/with-docker/Dockerfile @@ -25,10 +25,13 @@ RUN adduser -S nextjs -u 1001 # You only need to copy next.config.js if you are NOT using the default configuration # COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + USER nextjs EXPOSE 3000 @@ -40,4 +43,4 @@ ENV PORT 3000 # Uncomment the following line in case you want to disable telemetry. # ENV NEXT_TELEMETRY_DISABLED 1 -CMD ["node_modules/.bin/next", "start"] +CMD ["node", "server.js"] diff --git a/examples/with-docker/next.config.js b/examples/with-docker/next.config.js new file mode 100644 index 0000000000000..0568ecc9e5401 --- /dev/null +++ b/examples/with-docker/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + outputStandalone: true, + }, +} From 4c8415091c5edaa7fc4977eedc4b9df9dfe4a225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 13 Dec 2021 23:24:32 +0900 Subject: [PATCH 2/5] Update `jsx` transform of swc (#32383) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` This PR applies - https://github.com/swc-project/swc/pull/2741 This fixes `development` mode of jsx. --- packages/next-swc/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index 46ad7ec1644e0..ee440dffd1102 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -1918,9 +1918,9 @@ dependencies = [ [[package]] name = "swc_css_parser" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e320dae7460f78c5496bfd58b7a3494addd34a8df8841e689774cdecf760b4" +checksum = "c75f4add57624662c408f1fe61a8a14522a16dce24d5114885652701b6921f4f" dependencies = [ "bitflags", "lexical", @@ -2273,9 +2273,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_react" -version = "0.65.0" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f9a87fba33abfae51b6442c521af5bc607fe81aca98efb131102eff2b3df38" +checksum = "99f7d18dca4fbdc563244658bcea15b8151cd37ae7462588bf4b15eada4e5688" dependencies = [ "ahash", "base64 0.13.0", From d66579409ed01952c6661322c9a07ad620df192c Mon Sep 17 00:00:00 2001 From: xiaohai Date: Mon, 13 Dec 2021 23:19:39 +0800 Subject: [PATCH 3/5] docs: remove empty example link (#32439) https://github.com/vercel/next.js/tree/canary/examples/custom-server This example has been moved to the documentation ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint` --- docs/advanced-features/custom-server.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/advanced-features/custom-server.md b/docs/advanced-features/custom-server.md index 9fcba6659183a..d476b52c08edc 100644 --- a/docs/advanced-features/custom-server.md +++ b/docs/advanced-features/custom-server.md @@ -7,7 +7,6 @@ description: Start a Next.js app programmatically using a custom server.
Examples
    -
  • Basic custom server
  • Express integration
  • Hapi integration
  • Koa integration
  • From c658fd327648035e9c9c6ef9faa02e193d13dd44 Mon Sep 17 00:00:00 2001 From: Thomas Knickman Date: Mon, 13 Dec 2021 10:25:45 -0500 Subject: [PATCH 4/5] chore(blog-starter): update tailwindcss to v3 (#32398) Updates the [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) example to use the new [tailwindcss v3](https://tailwindcss.com/blog/tailwindcss-v3) by following the [upgrade guide](https://tailwindcss.com/docs/upgrade-guide). Thanks! ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint` --- examples/blog-starter/README.md | 2 +- examples/blog-starter/package.json | 6 +++--- examples/blog-starter/styles/index.css | 12 ------------ examples/blog-starter/tailwind.config.js | 2 +- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/examples/blog-starter/README.md b/examples/blog-starter/README.md index beb978227b037..cdeb9747b5f30 100644 --- a/examples/blog-starter/README.md +++ b/examples/blog-starter/README.md @@ -61,4 +61,4 @@ Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&ut # Notes -This blog-starter uses [Tailwind CSS](https://tailwindcss.com). To control the generated stylesheet's filesize, this example uses Tailwind CSS' v2.0 [`purge` option](https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css) to remove unused CSS. +This blog-starter uses [Tailwind CSS](https://tailwindcss.com) [(v3.0)](https://tailwindcss.com/blog/tailwindcss-v3). diff --git a/examples/blog-starter/package.json b/examples/blog-starter/package.json index 82d61419b449a..1e6a6ae24c82c 100644 --- a/examples/blog-starter/package.json +++ b/examples/blog-starter/package.json @@ -16,8 +16,8 @@ "remark-html": "13.0.1" }, "devDependencies": { - "autoprefixer": "^10.2.1", - "postcss": "^8.2.4", - "tailwindcss": "^2.0.2" + "autoprefixer": "^10.4.0", + "postcss": "^8.4.4", + "tailwindcss": "^3.0.1" } } diff --git a/examples/blog-starter/styles/index.css b/examples/blog-starter/styles/index.css index 719e6c05b0d76..b5c61c956711f 100644 --- a/examples/blog-starter/styles/index.css +++ b/examples/blog-starter/styles/index.css @@ -1,15 +1,3 @@ @tailwind base; - -/* Write your own custom base styles here */ - -/* Start purging... */ @tailwind components; -/* Stop purging. */ - -/* Write you own custom component styles here */ - -/* Start purging... */ @tailwind utilities; -/* Stop purging. */ - -/* Your own custom utilities */ diff --git a/examples/blog-starter/tailwind.config.js b/examples/blog-starter/tailwind.config.js index e32267d853ffe..b176069a2c6b9 100644 --- a/examples/blog-starter/tailwind.config.js +++ b/examples/blog-starter/tailwind.config.js @@ -1,5 +1,5 @@ module.exports = { - purge: ['./components/**/*.js', './pages/**/*.js'], + content: ['./components/**/*.js', './pages/**/*.js'], theme: { extend: { colors: { From 59f7676966570093f56ff35fb0d83eb97b394b7b Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Mon, 13 Dec 2021 19:30:24 +0100 Subject: [PATCH 5/5] Fix running server with Polyfilled fetch (#32368) **Note**: This PR is applying again changes landed #31935 that were reverted from an investigation. This PR fixes #30398 By default Next will polyfill some fetch APIs (Request, Response, Header and fetch) only if fetch is not found in the global scope in certain entry points. If we have a custom server which is adding a global fetch (and only fetch) at the very top then the rest of APIs will not be polyfilled. This PR adds a test on the custom server where we can add a custom polyfill for fetch with an env variable. This reproduces the issue since next-server.js will be required without having a polyfill for Response which makes it fail on requiring NextResponse. Then we remove the code that checks for subrequests to happen within the **sandbox** so that we don't need to polyfill `next-server` anymore. The we also introduce an improvement on how we handle relative requests. Since #31858 introduced a `port` and `hostname` options for the server, we can always pass absolute URLs to the Middleware so we can always use the original `nextUrl` to pass it to fetch. This brings a lot of simplification for `NextURL` since we don't have to consider relative URLs no more. ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` --- packages/next/server/base-server.ts | 50 +++-- packages/next/server/web/adapter.ts | 9 +- packages/next/server/web/next-url.ts | 211 ++++++++++-------- packages/next/server/web/sandbox/sandbox.ts | 13 ++ .../shared/lib/router/utils/relativize-url.ts | 13 ++ test/integration/custom-server/server.js | 4 + .../custom-server/test/index.test.js | 10 + .../core/pages/interface/[id]/_middleware.js | 7 + .../core/pages/interface/_middleware.js | 4 +- .../core/pages/responses/_middleware.js | 6 +- .../middleware/core/test/index.test.js | 8 + test/unit/web-runtime/next-url.test.ts | 117 ++++++---- 12 files changed, 287 insertions(+), 165 deletions(-) create mode 100644 packages/next/shared/lib/router/utils/relativize-url.ts create mode 100644 test/integration/middleware/core/pages/interface/[id]/_middleware.js diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 47bab324ea7af..6293360db00f5 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -91,10 +91,10 @@ import { parseNextUrl } from '../shared/lib/router/utils/parse-next-url' import isError from '../lib/is-error' import { getMiddlewareInfo } from './require' import { MIDDLEWARE_ROUTE } from '../lib/constants' -import { NextResponse } from './web/spec-extension/response' import { run } from './web/sandbox' import { addRequestMeta, getRequestMeta } from './request-meta' import { toNodeHeaders } from './web/utils' +import { relativizeURL } from '../shared/lib/router/utils/relativize-url' const getCustomRouteMatcher = pathMatch(true) @@ -379,7 +379,13 @@ export default abstract class Server { parsedUrl.query = parseQs(parsedUrl.query) } - addRequestMeta(req, '__NEXT_INIT_URL', req.url) + // When there are hostname and port we build an absolute URL + const initUrl = + this.hostname && this.port + ? `http://${this.hostname}:${this.port}${req.url}` + : req.url + + addRequestMeta(req, '__NEXT_INIT_URL', initUrl) addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query }) const url = parseNextUrl({ @@ -673,6 +679,14 @@ export default abstract class Server { }): Promise { this.middlewareBetaWarning() + // For middleware to "fetch" we must always provide an absolute URL + const url = getRequestMeta(params.request, '__NEXT_INIT_URL')! + if (!url.startsWith('http')) { + throw new Error( + 'To use middleware you must provide a `hostname` and `port` to the Next.js Server' + ) + } + const page: { name?: string; params?: { [key: string]: string } } = {} if (await this.hasPage(params.parsedUrl.pathname)) { page.name = params.parsedUrl.pathname @@ -687,8 +701,6 @@ export default abstract class Server { } } - const subreq = params.request.headers[`x-middleware-subrequest`] - const subrequests = typeof subreq === 'string' ? subreq.split(':') : [] const allHeaders = new Headers() let result: FetchEventResult | null = null @@ -708,14 +720,6 @@ export default abstract class Server { serverless: this._isLikeServerless, }) - if (subrequests.includes(middlewareInfo.name)) { - result = { - response: NextResponse.next(), - waitUntil: Promise.resolve(), - } - continue - } - result = await run({ name: middlewareInfo.name, paths: middlewareInfo.paths, @@ -727,7 +731,7 @@ export default abstract class Server { i18n: this.nextConfig.i18n, trailingSlash: this.nextConfig.trailingSlash, }, - url: getRequestMeta(params.request, '__NEXT_INIT_URL')!, + url: url, page: page, }, useCache: !this.nextConfig.experimental.concurrentFeatures, @@ -1185,9 +1189,13 @@ export default abstract class Server { type: 'route', name: 'middleware catchall', fn: async (req, res, _params, parsed) => { - const fullUrl = getRequestMeta(req, '__NEXT_INIT_URL') + if (!this.middleware?.length) { + return { finished: false } + } + + const initUrl = getRequestMeta(req, '__NEXT_INIT_URL')! const parsedUrl = parseNextUrl({ - url: fullUrl, + url: initUrl, headers: req.headers, nextConfig: { basePath: this.nextConfig.basePath, @@ -1226,6 +1234,18 @@ export default abstract class Server { return { finished: true } } + if (result.response.headers.has('x-middleware-rewrite')) { + const value = result.response.headers.get('x-middleware-rewrite')! + const rel = relativizeURL(value, initUrl) + result.response.headers.set('x-middleware-rewrite', rel) + } + + if (result.response.headers.has('Location')) { + const value = result.response.headers.get('Location')! + const rel = relativizeURL(value, initUrl) + result.response.headers.set('Location', rel) + } + if ( !result.response.headers.has('x-middleware-rewrite') && !result.response.headers.has('x-middleware-next') && diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index 33325dc873776..ff7f3559453c3 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -1,8 +1,9 @@ import type { NextMiddleware, RequestData, FetchEventResult } from './types' +import type { RequestInit } from './spec-extension/request' import { DeprecationError } from './error' import { fromNodeHeaders } from './utils' import { NextFetchEvent } from './spec-extension/fetch-event' -import { NextRequest, RequestInit } from './spec-extension/request' +import { NextRequest } from './spec-extension/request' import { NextResponse } from './spec-extension/response' import { waitUntilSymbol } from './spec-compliant/fetch-event' @@ -11,13 +12,9 @@ export async function adapter(params: { page: string request: RequestData }): Promise { - const url = params.request.url.startsWith('/') - ? `https://${params.request.headers.host}${params.request.url}` - : params.request.url - const request = new NextRequestHint({ page: params.page, - input: url, + input: params.request.url, init: { geo: params.request.geo, headers: fromNodeHeaders(params.request.headers), diff --git a/packages/next/server/web/next-url.ts b/packages/next/server/web/next-url.ts index cba854d0d3079..8ee57ff66a6cb 100644 --- a/packages/next/server/web/next-url.ts +++ b/packages/next/server/web/next-url.ts @@ -1,66 +1,78 @@ import type { PathLocale } from '../../shared/lib/i18n/normalize-locale-path' import type { DomainLocale, I18NConfig } from '../config-shared' import { getLocaleMetadata } from '../../shared/lib/i18n/get-locale-metadata' -import cookie from 'next/dist/compiled/cookie' import { replaceBasePath } from '../router' - -/** - * TODO - * - * - Add comments to the URLNext API. - * - Move internals to be using symbols for its shape. - * - Make sure logging does not show any implementation details. - * - Include in the event payload the nextJS configuration - */ +import cookie from 'next/dist/compiled/cookie' interface Options { + base?: string | URL basePath?: string headers?: { [key: string]: string | string[] | undefined } i18n?: I18NConfig | null trailingSlash?: boolean } -const REGEX_LOCALHOST_HOSTNAME = - /(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1)/ - -export class NextURL extends URL { - private _basePath: string - private _locale?: { - defaultLocale: string - domain?: DomainLocale - locale: string - path: PathLocale - redirect?: string - trailingSlash?: boolean - } - private _options: Options - private _url: URL - - constructor(input: string, options: Options = {}) { - const url = createWHATWGURL(input) - super(url) - this._options = options - this._basePath = '' - this._url = url - this.analyzeUrl() +const Internal = Symbol('NextURLInternal') + +export class NextURL { + [Internal]: { + url: URL + options: Options + basePath: string + locale?: { + defaultLocale: string + domain?: DomainLocale + locale: string + path: PathLocale + redirect?: string + trailingSlash?: boolean + } } - get absolute() { - return this._url.hostname !== 'localhost' + constructor(input: string | URL, base?: string | URL, opts?: Options) + constructor(input: string | URL, opts?: Options) + constructor( + input: string | URL, + baseOrOpts?: string | URL | Options, + opts?: Options + ) { + let base: undefined | string | URL + let options: Options + + if ( + (typeof baseOrOpts === 'object' && 'pathname' in baseOrOpts) || + typeof baseOrOpts === 'string' + ) { + base = baseOrOpts + options = opts || {} + } else { + options = opts || baseOrOpts || {} + } + + this[Internal] = { + url: parseURL(input, base ?? options.base), + options: options, + basePath: '', + } + + this.analyzeUrl() } - analyzeUrl() { - const { headers = {}, basePath, i18n } = this._options + private analyzeUrl() { + const { headers = {}, basePath, i18n } = this[Internal].options - if (basePath && this._url.pathname.startsWith(basePath)) { - this._url.pathname = replaceBasePath(this._url.pathname, basePath) - this._basePath = basePath + if (basePath && this[Internal].url.pathname.startsWith(basePath)) { + this[Internal].url.pathname = replaceBasePath( + this[Internal].url.pathname, + basePath + ) + this[Internal].basePath = basePath } else { - this._basePath = '' + this[Internal].basePath = '' } if (i18n) { - this._locale = getLocaleMetadata({ + this[Internal].locale = getLocaleMetadata({ cookies: () => { const value = headers['cookie'] return value @@ -73,154 +85,156 @@ export class NextURL extends URL { i18n: i18n, }, url: { - hostname: this._url.hostname || null, - pathname: this._url.pathname, + hostname: this[Internal].url.hostname || null, + pathname: this[Internal].url.pathname, }, }) - if (this._locale?.path.detectedLocale) { - this._url.pathname = this._locale.path.pathname + if (this[Internal].locale?.path.detectedLocale) { + this[Internal].url.pathname = this[Internal].locale!.path.pathname } } } - formatPathname() { - const { i18n } = this._options - let pathname = this._url.pathname + private formatPathname() { + const { i18n } = this[Internal].options + let pathname = this[Internal].url.pathname - if (this._locale?.locale && i18n?.defaultLocale !== this._locale?.locale) { - pathname = `/${this._locale?.locale}${pathname}` + if ( + this[Internal].locale?.locale && + i18n?.defaultLocale !== this[Internal].locale?.locale + ) { + pathname = `/${this[Internal].locale?.locale}${pathname}` } - if (this._basePath) { - pathname = `${this._basePath}${pathname}` + if (this[Internal].basePath) { + pathname = `${this[Internal].basePath}${pathname}` } return pathname } - get locale() { - if (!this._locale) { - throw new TypeError(`The URL is not configured with i18n`) - } - - return this._locale.locale + public get locale() { + return this[Internal].locale?.locale ?? '' } - set locale(locale: string) { - if (!this._locale) { - throw new TypeError(`The URL is not configured with i18n`) + public set locale(locale: string) { + if ( + !this[Internal].locale || + !this[Internal].options.i18n?.locales.includes(locale) + ) { + throw new TypeError( + `The NextURL configuration includes no locale "${locale}"` + ) } - this._locale.locale = locale + this[Internal].locale!.locale = locale } get defaultLocale() { - return this._locale?.defaultLocale + return this[Internal].locale?.defaultLocale } get domainLocale() { - return this._locale?.domain + return this[Internal].locale?.domain } get searchParams() { - return this._url.searchParams + return this[Internal].url.searchParams } get host() { - return this.absolute ? this._url.host : '' + return this[Internal].url.host } set host(value: string) { - this._url.host = value + this[Internal].url.host = value } get hostname() { - return this.absolute ? this._url.hostname : '' + return this[Internal].url.hostname } set hostname(value: string) { - this._url.hostname = value || 'localhost' + this[Internal].url.hostname = value } get port() { - return this.absolute ? this._url.port : '' + return this[Internal].url.port } set port(value: string) { - this._url.port = value + this[Internal].url.port = value } get protocol() { - return this.absolute ? this._url.protocol : '' + return this[Internal].url.protocol } set protocol(value: string) { - this._url.protocol = value + this[Internal].url.protocol = value } get href() { const pathname = this.formatPathname() - return this.absolute - ? `${this.protocol}//${this.host}${pathname}${this._url.search}` - : `${pathname}${this._url.search}` + return `${this.protocol}//${this.host}${pathname}${this[Internal].url.search}` } set href(url: string) { - this._url = createWHATWGURL(url) + this[Internal].url = parseURL(url) this.analyzeUrl() } get origin() { - return this.absolute ? this._url.origin : '' + return this[Internal].url.origin } get pathname() { - return this._url.pathname + return this[Internal].url.pathname } set pathname(value: string) { - this._url.pathname = value + this[Internal].url.pathname = value } get hash() { - return this._url.hash + return this[Internal].url.hash } set hash(value: string) { - this._url.hash = value + this[Internal].url.hash = value } get search() { - return this._url.search + return this[Internal].url.search } set search(value: string) { - this._url.search = value + this[Internal].url.search = value } get password() { - return this._url.password + return this[Internal].url.password } set password(value: string) { - this._url.password = value + this[Internal].url.password = value } get username() { - return this._url.username + return this[Internal].url.username } set username(value: string) { - this._url.username = value + this[Internal].url.username = value } get basePath() { - return this._basePath + return this[Internal].basePath } set basePath(value: string) { - this._basePath = value.startsWith('/') ? value : `/${value}` + this[Internal].basePath = value.startsWith('/') ? value : `/${value}` } toString() { @@ -232,13 +246,12 @@ export class NextURL extends URL { } } -function createWHATWGURL(url: string) { - url = url.replace(REGEX_LOCALHOST_HOSTNAME, 'localhost') - return isRelativeURL(url) - ? new URL(url.replace(/^\/+/, '/'), new URL('https://localhost')) - : new URL(url) -} +const REGEX_LOCALHOST_HOSTNAME = + /(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1|localhost)/ -function isRelativeURL(url: string) { - return url.startsWith('/') +function parseURL(url: string | URL, base?: string | URL) { + return new URL( + String(url).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost'), + base && String(base).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost') + ) } diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 6fb8012c921aa..7a98d4d122343 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -18,6 +18,19 @@ export async function run(params: { runInContext(paramPath) } + const subreq = params.request.headers[`x-middleware-subrequest`] + const subrequests = typeof subreq === 'string' ? subreq.split(':') : [] + if (subrequests.includes(params.name)) { + return { + waitUntil: Promise.resolve(), + response: new context.Response(null, { + headers: { + 'x-middleware-next': '1', + }, + }), + } + } + return context._ENTRIES[`middleware_${params.name}`].default({ request: params.request, }) diff --git a/packages/next/shared/lib/router/utils/relativize-url.ts b/packages/next/shared/lib/router/utils/relativize-url.ts new file mode 100644 index 0000000000000..2bc97102f08e6 --- /dev/null +++ b/packages/next/shared/lib/router/utils/relativize-url.ts @@ -0,0 +1,13 @@ +/** + * Given a URL as a string and a base URL it will make the URL relative + * if the parsed protocol and host is the same as the one in the base + * URL. Otherwise it returns the same URL string. + */ +export function relativizeURL(url: string | string, base: string | URL) { + const baseURL = typeof base === 'string' ? new URL(base) : base + const relative = new URL(url, base) + const origin = `${baseURL.protocol}//${baseURL.host}` + return `${relative.protocol}//${relative.host}` === origin + ? relative.toString().replace(origin, '') + : relative.toString() +} diff --git a/test/integration/custom-server/server.js b/test/integration/custom-server/server.js index 0b34500af9e57..29550fb1906f4 100644 --- a/test/integration/custom-server/server.js +++ b/test/integration/custom-server/server.js @@ -1,3 +1,7 @@ +if (process.env.POLYFILL_FETCH) { + global.fetch = require('node-fetch').default +} + const http = require('http') const next = require('next') diff --git a/test/integration/custom-server/test/index.test.js b/test/integration/custom-server/test/index.test.js index 272000993f8f2..ef0dee74715b5 100644 --- a/test/integration/custom-server/test/index.test.js +++ b/test/integration/custom-server/test/index.test.js @@ -195,4 +195,14 @@ describe('Custom Server', () => { } ) }) + + describe('with a custom fetch polyfill', () => { + beforeAll(() => startServer({ POLYFILL_FETCH: 'true' })) + afterAll(() => killApp(server)) + + it('should serve internal file from render', async () => { + const data = await renderViaHTTP(appPort, '/static/hello.txt') + expect(data).toMatch(/hello world/) + }) + }) }) diff --git a/test/integration/middleware/core/pages/interface/[id]/_middleware.js b/test/integration/middleware/core/pages/interface/[id]/_middleware.js new file mode 100644 index 0000000000000..679ab02834e00 --- /dev/null +++ b/test/integration/middleware/core/pages/interface/[id]/_middleware.js @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' + +export function middleware() { + const response = NextResponse.next() + response.headers.set('x-dynamic-path', 'true') + return response +} diff --git a/test/integration/middleware/core/pages/interface/_middleware.js b/test/integration/middleware/core/pages/interface/_middleware.js index a3ac20fd7c379..53e52ae12f1d7 100644 --- a/test/integration/middleware/core/pages/interface/_middleware.js +++ b/test/integration/middleware/core/pages/interface/_middleware.js @@ -54,9 +54,7 @@ export async function middleware(request) { } if (url.pathname.endsWith('/root-subrequest')) { - return fetch( - `http://${request.headers.get('host')}/interface/root-subrequest` - ) + return fetch(url) } return new Response(null, { diff --git a/test/integration/middleware/core/pages/responses/_middleware.js b/test/integration/middleware/core/pages/responses/_middleware.js index 255f1c3180a1b..a19326aad8f0f 100644 --- a/test/integration/middleware/core/pages/responses/_middleware.js +++ b/test/integration/middleware/core/pages/responses/_middleware.js @@ -57,11 +57,11 @@ export async function middleware(request, ev) { ev.waitUntil( (async () => { writer.write(encoder.encode('this is a streamed '.repeat(10))) - await sleep(2000) + await sleep(200) writer.write(encoder.encode('after 2 seconds '.repeat(10))) - await sleep(2000) + await sleep(200) writer.write(encoder.encode('after 4 seconds '.repeat(10))) - await sleep(2000) + await sleep(200) writer.close() })() ) diff --git a/test/integration/middleware/core/test/index.test.js b/test/integration/middleware/core/test/index.test.js index f0e10c2328414..6c3e90e01a56a 100644 --- a/test/integration/middleware/core/test/index.test.js +++ b/test/integration/middleware/core/test/index.test.js @@ -447,6 +447,14 @@ function interfaceTests(locale = '') { const element = await browser.elementByCss('.title') expect(await element.text()).toEqual('Dynamic route') }) + + it(`${locale} allows subrequests without infinite loops`, async () => { + const res = await fetchViaHTTP( + context.appPort, + `/interface/root-subrequest` + ) + expect(res.headers.get('x-dynamic-path')).toBe('true') + }) } function getCookieFromResponse(res, cookieName) { diff --git a/test/unit/web-runtime/next-url.test.ts b/test/unit/web-runtime/next-url.test.ts index 43508d4d3253e..e3914a938e920 100644 --- a/test/unit/web-runtime/next-url.test.ts +++ b/test/unit/web-runtime/next-url.test.ts @@ -1,74 +1,70 @@ /* eslint-env jest */ import { NextURL } from 'next/dist/server/web/next-url' -it('has the right shape', () => { - const parsed = new NextURL('/about?param1=value1') +// TODO Make NextURL extend URL +it.skip('has the right shape and prototype', () => { + const parsed = new NextURL('/about?param1=value1', 'http://127.0.0.1') expect(parsed).toBeInstanceOf(URL) }) -it('allows to format relative urls', async () => { - const parsed = new NextURL('/about?param1=value1') +it('allows to the pathname', async () => { + const parsed = new NextURL('/about?param1=value1', 'http://127.0.0.1:3000') expect(parsed.basePath).toEqual('') - expect(parsed.hostname).toEqual('') - expect(parsed.host).toEqual('') - expect(parsed.href).toEqual('/about?param1=value1') + expect(parsed.hostname).toEqual('localhost') + expect(parsed.host).toEqual('localhost:3000') + expect(parsed.href).toEqual('http://localhost:3000/about?param1=value1') parsed.pathname = '/hihi' - expect(parsed.href).toEqual('/hihi?param1=value1') + expect(parsed.href).toEqual('http://localhost:3000/hihi?param1=value1') }) -it('allows to change the host of a relative url', () => { - const parsed = new NextURL('/about?param1=value1') - expect(parsed.hostname).toEqual('') - expect(parsed.host).toEqual('') - expect(parsed.href).toEqual('/about?param1=value1') +it('allows to change the host', () => { + const parsed = new NextURL('/about?param1=value1', 'http://127.0.0.1') + expect(parsed.hostname).toEqual('localhost') + expect(parsed.host).toEqual('localhost') + expect(parsed.href).toEqual('http://localhost/about?param1=value1') parsed.hostname = 'foo.com' expect(parsed.hostname).toEqual('foo.com') expect(parsed.host).toEqual('foo.com') - expect(parsed.href).toEqual('https://foo.com/about?param1=value1') - expect(parsed.toString()).toEqual('https://foo.com/about?param1=value1') + expect(parsed.href).toEqual('http://foo.com/about?param1=value1') + expect(parsed.toString()).toEqual('http://foo.com/about?param1=value1') }) -it('allows to change the hostname of a relative url', () => { - const url = new NextURL('/example') - url.hostname = 'foo.com' - expect(url.toString()).toEqual('https://foo.com/example') -}) - -it('allows to remove the hostname of an absolute url', () => { +it('does noop changing to an invalid hostname', () => { const url = new NextURL('https://foo.com/example') url.hostname = '' - expect(url.toString()).toEqual('/example') + expect(url.toString()).toEqual('https://foo.com/example') }) -it('allows to change the whole href of an absolute url', () => { +it('allows to change the whole href', () => { const url = new NextURL('https://localhost.com/foo') expect(url.hostname).toEqual('localhost.com') expect(url.protocol).toEqual('https:') expect(url.host).toEqual('localhost.com') - url.href = '/foo' - expect(url.hostname).toEqual('') - expect(url.protocol).toEqual('') - expect(url.host).toEqual('') + url.href = 'http://foo.com/bar' + expect(url.hostname).toEqual('foo.com') + expect(url.protocol).toEqual('http:') + expect(url.host).toEqual('foo.com') }) it('allows to update search params', () => { - const url = new NextURL('/example') + const url = new NextURL('/example', 'http://localhost.com') url.searchParams.set('foo', 'bar') expect(url.search).toEqual('?foo=bar') - expect(url.toString()).toEqual('/example?foo=bar') + expect(url.toString()).toEqual('http://localhost.com/example?foo=bar') }) it('parses and formats the basePath', () => { const url = new NextURL('/root/example', { + base: 'http://127.0.0.1', basePath: '/root', }) expect(url.basePath).toEqual('/root') expect(url.pathname).toEqual('/example') - expect(url.toString()).toEqual('/root/example') + expect(url.toString()).toEqual('http://localhost/root/example') const url2 = new NextURL('https://foo.com/root/bar', { basePath: '/root', @@ -89,14 +85,47 @@ it('parses and formats the basePath', () => { expect(url3.basePath).toEqual('') - url3.href = '/root/example' - expect(url.basePath).toEqual('/root') - expect(url.pathname).toEqual('/example') - expect(url.toString()).toEqual('/root/example') + url3.href = 'http://localhost.com/root/example' + expect(url3.basePath).toEqual('/root') + expect(url3.pathname).toEqual('/example') + expect(url3.toString()).toEqual('http://localhost.com/root/example') +}) + +it('allows to get empty locale when there is no locale', () => { + const url = new NextURL('https://localhost:3000/foo') + expect(url.locale).toEqual('') +}) + +it('doesnt allow to set an unexisting locale', () => { + const url = new NextURL('https://localhost:3000/foo') + let error: Error | null = null + try { + url.locale = 'foo' + } catch (err) { + error = err + } + + expect(error).toBeInstanceOf(TypeError) + expect(error.message).toEqual( + 'The NextURL configuration includes no locale "foo"' + ) +}) + +it('always get a default locale', () => { + const url = new NextURL('/bar', { + base: 'http://127.0.0.1', + i18n: { + defaultLocale: 'en', + locales: ['en', 'es', 'fr'], + }, + }) + + expect(url.locale).toEqual('en') }) it('parses and formats the default locale', () => { const url = new NextURL('/es/bar', { + base: 'http://127.0.0.1', basePath: '/root', i18n: { defaultLocale: 'en', @@ -105,19 +134,19 @@ it('parses and formats the default locale', () => { }) expect(url.locale).toEqual('es') - expect(url.toString()).toEqual('/es/bar') + expect(url.toString()).toEqual('http://localhost/es/bar') url.basePath = '/root' expect(url.locale).toEqual('es') - expect(url.toString()).toEqual('/root/es/bar') + expect(url.toString()).toEqual('http://localhost/root/es/bar') url.locale = 'en' expect(url.locale).toEqual('en') - expect(url.toString()).toEqual('/root/bar') + expect(url.toString()).toEqual('http://localhost/root/bar') url.locale = 'fr' expect(url.locale).toEqual('fr') - expect(url.toString()).toEqual('/root/fr/bar') + expect(url.toString()).toEqual('http://localhost/root/fr/bar') }) it('consider 127.0.0.1 and variations as localhost', () => { @@ -131,3 +160,13 @@ it('consider 127.0.0.1 and variations as localhost', () => { expect(new NextURL('https://127.0.1.0:3000/hello')).toStrictEqual(httpsUrl) expect(new NextURL('https://::1:3000/hello')).toStrictEqual(httpsUrl) }) + +it('allows to change the port', () => { + const url = new NextURL('https://localhost:3000/foo') + url.port = '3001' + expect(url.href).toEqual('https://localhost:3001/foo') + url.port = '80' + expect(url.href).toEqual('https://localhost:80/foo') + url.port = '' + expect(url.href).toEqual('https://localhost/foo') +})