Skip to content

Commit a815ba9

Browse files
authored
Implement Middleware RFC (#30081)
This PR adds support for [Middleware as per RFC ](#29750). ## 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
1 parent 157302d commit a815ba9

File tree

94 files changed

+4805
-160
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+4805
-160
lines changed

packages/next/build/entries.ts

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import chalk from 'chalk'
22
import { posix, join } from 'path'
33
import { stringify } from 'querystring'
44
import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants'
5+
import { MIDDLEWARE_ROUTE } from '../lib/constants'
56
import { __ApiPreviewProps } from '../server/api-utils'
67
import { isTargetLikeServerless } from '../server/config'
78
import { normalizePagePath } from '../server/normalize-page-path'
89
import { warn } from './output/log'
10+
import { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader'
911
import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader'
1012
import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
1113
import { LoadedEnvFiles } from '@next/env'
1214
import { NextConfigComplete } from '../server/config-shared'
1315
import type webpack5 from 'webpack5'
1416

17+
type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
1518
type PagesMapping = {
1619
[page: string]: string
1720
}
@@ -118,6 +121,18 @@ export function createEntrypoints(
118121

119122
const isLikeServerless = isTargetLikeServerless(target)
120123

124+
if (page.match(MIDDLEWARE_ROUTE)) {
125+
const loaderOpts: MiddlewareLoaderOptions = {
126+
absolutePagePath: pages[page],
127+
page,
128+
}
129+
130+
client[clientBundlePath] = `next-middleware-loader?${stringify(
131+
loaderOpts
132+
)}!`
133+
return
134+
}
135+
121136
if (isApiRoute && isLikeServerless) {
122137
const serverlessLoaderOptions: ServerlessLoaderQuery = {
123138
page,
@@ -170,54 +185,56 @@ export function createEntrypoints(
170185
}
171186
}
172187

173-
export function finalizeEntrypoint(
174-
name: string,
175-
value: any,
188+
export function finalizeEntrypoint({
189+
name,
190+
value,
191+
isServer,
192+
}: {
176193
isServer: boolean
177-
): any {
194+
name: string
195+
value: ObjectValue<webpack5.EntryObject>
196+
}): ObjectValue<webpack5.EntryObject> {
197+
const entry =
198+
typeof value !== 'object' || Array.isArray(value)
199+
? { import: value }
200+
: value
201+
178202
if (isServer) {
179203
const isApi = name.startsWith('pages/api/')
180-
const runtime = isApi ? 'webpack-api-runtime' : 'webpack-runtime'
181-
const layer = isApi ? 'api' : undefined
182-
const publicPath = isApi ? '' : undefined
183-
if (typeof value === 'object' && !Array.isArray(value)) {
184-
return {
185-
publicPath,
186-
runtime,
187-
layer,
188-
...value,
189-
}
190-
} else {
191-
return {
192-
import: value,
193-
publicPath,
194-
runtime,
195-
layer,
196-
}
204+
return {
205+
publicPath: isApi ? '' : undefined,
206+
runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
207+
layer: isApi ? 'api' : undefined,
208+
...entry,
197209
}
198-
} else {
199-
if (
200-
name !== 'polyfills' &&
201-
name !== 'main' &&
202-
name !== 'amp' &&
203-
name !== 'react-refresh'
204-
) {
205-
const dependOn =
210+
}
211+
212+
if (name.match(MIDDLEWARE_ROUTE)) {
213+
return {
214+
filename: 'server/[name].js',
215+
layer: 'middleware',
216+
library: {
217+
name: ['_ENTRIES', `middleware_[name]`],
218+
type: 'assign',
219+
},
220+
...entry,
221+
}
222+
}
223+
224+
if (
225+
name !== 'polyfills' &&
226+
name !== 'main' &&
227+
name !== 'amp' &&
228+
name !== 'react-refresh'
229+
) {
230+
return {
231+
dependOn:
206232
name.startsWith('pages/') && name !== 'pages/_app'
207233
? 'pages/_app'
208-
: 'main'
209-
if (typeof value === 'object' && !Array.isArray(value)) {
210-
return {
211-
dependOn,
212-
...value,
213-
}
214-
} else {
215-
return {
216-
import: value,
217-
dependOn,
218-
}
219-
}
234+
: 'main',
235+
...entry,
220236
}
221237
}
222-
return value
238+
239+
return entry
223240
}

packages/next/build/index.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-me
1313
import {
1414
STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
1515
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
16+
MIDDLEWARE_ROUTE,
1617
} from '../lib/constants'
1718
import { fileExists } from '../lib/file-exists'
1819
import { findPagesDir } from '../lib/find-pages-dir'
@@ -46,6 +47,7 @@ import {
4647
SERVER_DIRECTORY,
4748
SERVER_FILES_MANIFEST,
4849
STATIC_STATUS_PAGES,
50+
MIDDLEWARE_MANIFEST,
4951
} from '../shared/lib/constants'
5052
import {
5153
getRouteRegex,
@@ -94,6 +96,9 @@ import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
9496
import { NextConfigComplete } from '../server/config-shared'
9597
import isError from '../lib/is-error'
9698
import { TelemetryPlugin } from './webpack/plugins/telemetry-plugin'
99+
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
100+
101+
const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/
97102

98103
export type SsgRoute = {
99104
initialRevalidateSeconds: number | false
@@ -394,6 +399,12 @@ export default async function build(
394399
fallback: Array<ReturnType<typeof buildCustomRoute>>
395400
}
396401
headers: Array<ReturnType<typeof buildCustomRoute>>
402+
staticRoutes: Array<{
403+
page: string
404+
regex: string
405+
namedRegex?: string
406+
routeKeys?: { [key: string]: string }
407+
}>
397408
dynamicRoutes: Array<{
398409
page: string
399410
regex: string
@@ -424,16 +435,16 @@ export default async function build(
424435
redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')),
425436
headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
426437
dynamicRoutes: getSortedRoutes(pageKeys)
427-
.filter(isDynamicRoute)
428-
.map((page) => {
429-
const routeRegex = getRouteRegex(page)
430-
return {
431-
page,
432-
regex: normalizeRouteRegex(routeRegex.re.source),
433-
routeKeys: routeRegex.routeKeys,
434-
namedRegex: routeRegex.namedRegex,
435-
}
436-
}),
438+
.filter((page) => isDynamicRoute(page) && !page.match(MIDDLEWARE_ROUTE))
439+
.map(pageToRoute),
440+
staticRoutes: getSortedRoutes(pageKeys)
441+
.filter(
442+
(page) =>
443+
!isDynamicRoute(page) &&
444+
!page.match(MIDDLEWARE_ROUTE) &&
445+
!page.match(RESERVED_PAGE)
446+
)
447+
.map(pageToRoute),
437448
dataRoutes: [],
438449
i18n: config.i18n || undefined,
439450
}))
@@ -833,11 +844,7 @@ export default async function build(
833844
let isHybridAmp = false
834845
let ssgPageRoutes: string[] | null = null
835846

836-
const nonReservedPage = !page.match(
837-
/^\/(_app|_error|_document|api(\/|$))/
838-
)
839-
840-
if (nonReservedPage) {
847+
if (!page.match(MIDDLEWARE_ROUTE) && !page.match(RESERVED_PAGE)) {
841848
try {
842849
let isPageStaticSpan =
843850
checkPageSpan.traceChild('is-page-static')
@@ -1694,6 +1701,25 @@ export default async function build(
16941701
)
16951702
}
16961703

1704+
const middlewareManifest: MiddlewareManifest = JSON.parse(
1705+
await promises.readFile(
1706+
path.join(distDir, SERVER_DIRECTORY, MIDDLEWARE_MANIFEST),
1707+
'utf8'
1708+
)
1709+
)
1710+
1711+
await promises.writeFile(
1712+
path.join(
1713+
distDir,
1714+
CLIENT_STATIC_FILES_PATH,
1715+
buildId,
1716+
'_middlewareManifest.js'
1717+
),
1718+
`self.__MIDDLEWARE_MANIFEST=${devalue(
1719+
middlewareManifest.sortedMiddleware
1720+
)};self.__MIDDLEWARE_MANIFEST_CB&&self.__MIDDLEWARE_MANIFEST_CB()`
1721+
)
1722+
16971723
const images = { ...config.images }
16981724
const { deviceSizes, imageSizes } = images
16991725
;(images as any).sizes = [...deviceSizes, ...imageSizes]
@@ -1797,3 +1823,13 @@ function generateClientSsgManifest(
17971823
function isTelemetryPlugin(plugin: unknown): plugin is TelemetryPlugin {
17981824
return plugin instanceof TelemetryPlugin
17991825
}
1826+
1827+
function pageToRoute(page: string) {
1828+
const routeRegex = getRouteRegex(page)
1829+
return {
1830+
page,
1831+
regex: normalizeRouteRegex(routeRegex.re.source),
1832+
routeKeys: routeRegex.routeKeys,
1833+
namedRegex: routeRegex.namedRegex,
1834+
}
1835+
}

packages/next/build/webpack-config.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
NEXT_PROJECT_ROOT,
1414
NEXT_PROJECT_ROOT_DIST_CLIENT,
1515
PAGES_DIR_ALIAS,
16+
MIDDLEWARE_ROUTE,
1617
} from '../lib/constants'
1718
import { fileExists } from '../lib/file-exists'
1819
import { getPackageVersion } from '../lib/get-package-version'
@@ -34,6 +35,7 @@ import { finalizeEntrypoint } from './entries'
3435
import * as Log from './output/log'
3536
import { build as buildConfiguration } from './webpack/config'
3637
import { __overrideCssConfiguration } from './webpack/config/blocks/css/overrideCssConfiguration'
38+
import MiddlewarePlugin from './webpack/plugins/middleware-plugin'
3739
import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin'
3840
import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin'
3941
import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin'
@@ -323,6 +325,20 @@ export default async function getBaseWebpackConfig(
323325
hasJsxRuntime: true,
324326
},
325327
},
328+
babelMiddleware: {
329+
loader: require.resolve('./babel/loader/index'),
330+
options: {
331+
cache: false,
332+
configFile: babelConfigFile,
333+
cwd: dir,
334+
development: dev,
335+
distDir,
336+
hasJsxRuntime: true,
337+
hasReactRefresh: false,
338+
isServer: true,
339+
pagesDir,
340+
},
341+
},
326342
}
327343

328344
const babelIncludeRegexes: RegExp[] = [
@@ -576,10 +592,12 @@ export default async function getBaseWebpackConfig(
576592
// as we don't need a separate vendor chunk from that
577593
// and all other chunk depend on them so there is no
578594
// duplication that need to be pulled out.
579-
chunks: (chunk) => !/^(polyfills|main|pages\/_app)$/.test(chunk.name),
595+
chunks: (chunk) =>
596+
!/^(polyfills|main|pages\/_app|\/_middleware)$/.test(chunk.name),
580597
cacheGroups: {
581598
framework: {
582-
chunks: 'all',
599+
chunks: (chunk: webpack.compilation.Chunk) =>
600+
!chunk.name?.match(MIDDLEWARE_ROUTE),
583601
name: 'framework',
584602
// This regex ignores nested copies of framework libraries so they're
585603
// bundled with their issuer.
@@ -629,6 +647,12 @@ export default async function getBaseWebpackConfig(
629647
minChunks: totalPages,
630648
priority: 20,
631649
},
650+
middleware: {
651+
chunks: (chunk: webpack.compilation.Chunk) =>
652+
chunk.name?.match(MIDDLEWARE_ROUTE),
653+
filename: 'server/middleware-chunks/[name].js',
654+
minChunks: 2,
655+
},
632656
},
633657
maxInitialRequests: 25,
634658
minSize: 20000,
@@ -898,7 +922,7 @@ export default async function getBaseWebpackConfig(
898922
: ({
899923
filename: '[name].js',
900924
// allow to split entrypoints
901-
chunks: 'all',
925+
chunks: ({ name }: any) => !name?.match(MIDDLEWARE_ROUTE),
902926
// size of files is not so relevant for server build
903927
// we want to prefer deduplication to load less code
904928
minSize: 1000,
@@ -1001,6 +1025,7 @@ export default async function getBaseWebpackConfig(
10011025
'next-serverless-loader',
10021026
'next-style-loader',
10031027
'noop-loader',
1028+
'next-middleware-loader',
10041029
].reduce((alias, loader) => {
10051030
// using multiple aliases to replace `resolveLoader.modules`
10061031
alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader)
@@ -1046,6 +1071,11 @@ export default async function getBaseWebpackConfig(
10461071
},
10471072
use: defaultLoaders.babel,
10481073
},
1074+
{
1075+
...codeCondition,
1076+
issuerLayer: 'middleware',
1077+
use: defaultLoaders.babelMiddleware,
1078+
},
10491079
{
10501080
...codeCondition,
10511081
use: hasReactRefresh
@@ -1238,6 +1268,9 @@ export default async function getBaseWebpackConfig(
12381268
isServerless && isServer && new ServerlessPlugin(),
12391269
isServer &&
12401270
new PagesManifestPlugin({ serverless: isLikeServerless, dev }),
1271+
// MiddlewarePlugin should be after DefinePlugin so NEXT_PUBLIC_*
1272+
// replacement is done before its process.env.* handling
1273+
!isServer && new MiddlewarePlugin({ dev }),
12411274
isServer && new NextJsSsrImportPlugin(),
12421275
!isServer &&
12431276
new BuildManifestPlugin({
@@ -1304,6 +1337,10 @@ export default async function getBaseWebpackConfig(
13041337
},
13051338
}
13061339

1340+
if (!isServer) {
1341+
webpack5Config.output!.enabledLibraryTypes = ['assign']
1342+
}
1343+
13071344
if (dev) {
13081345
// @ts-ignore unsafeCache exists
13091346
webpack5Config.module.unsafeCache = (module) =>
@@ -1819,7 +1856,11 @@ export default async function getBaseWebpackConfig(
18191856
delete entry['main.js']
18201857

18211858
for (const name of Object.keys(entry)) {
1822-
entry[name] = finalizeEntrypoint(name, entry[name], isServer)
1859+
entry[name] = finalizeEntrypoint({
1860+
value: entry[name],
1861+
isServer,
1862+
name,
1863+
})
18231864
}
18241865

18251866
return entry

0 commit comments

Comments
 (0)