Skip to content

Commit c31d07f

Browse files
committed
Auto wrap middleware
1 parent abc8426 commit c31d07f

File tree

5 files changed

+115
-6
lines changed

5 files changed

+115
-6
lines changed

packages/nextjs/rollup.npm.config.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export default [
1414
),
1515
...makeNPMConfigVariants(
1616
makeBaseNPMConfig({
17-
entrypoints: ['src/config/templates/pageWrapperTemplate.ts', 'src/config/templates/apiWrapperTemplate.ts'],
17+
entrypoints: [
18+
'src/config/templates/pageWrapperTemplate.ts',
19+
'src/config/templates/apiWrapperTemplate.ts',
20+
'src/config/templates/middlewareWrapperTemplate.ts',
21+
],
1822

1923
packageSpecificConfig: {
2024
output: {

packages/nextjs/src/config/loaders/wrappingLoader.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encodin
1212
const pageWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'pageWrapperTemplate.js');
1313
const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encoding: 'utf8' });
1414

15+
const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js');
16+
const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' });
17+
1518
// Just a simple placeholder to make referencing module consistent
1619
const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';
1720

@@ -65,7 +68,17 @@ export default function wrappingLoader(
6568
return;
6669
}
6770

68-
let templateCode = parameterizedRoute.startsWith('/api') ? apiWrapperTemplateCode : pageWrapperTemplateCode;
71+
const middlewareJsPath = path.join(pagesDir, '..', 'middleware.js');
72+
const middlewareTsPath = path.join(pagesDir, '..', 'middleware.js');
73+
74+
let templateCode: string;
75+
if (parameterizedRoute.startsWith('/api')) {
76+
templateCode = apiWrapperTemplateCode;
77+
} else if (this.resourcePath === middlewareJsPath || this.resourcePath === middlewareTsPath) {
78+
templateCode = middlewareWrapperTemplateCode;
79+
} else {
80+
templateCode = pageWrapperTemplateCode;
81+
}
6982

7083
// Inject the route and the path to the file we're wrapping into the template
7184
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedRoute.replace(/\\/g, '\\\\'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* This file is a template for the code which will be substituted when our webpack loader handles API files in the
3+
* `pages/` directory.
4+
*
5+
* We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package,
6+
* this causes both TS and ESLint to complain, hence the pragma comments below.
7+
*/
8+
9+
// @ts-ignore See above
10+
// eslint-disable-next-line import/no-unresolved
11+
import * as origModule from '__SENTRY_WRAPPING_TARGET__';
12+
// eslint-disable-next-line import/no-extraneous-dependencies
13+
import * as Sentry from '@sentry/nextjs';
14+
15+
// We import this from `wrappers` rather than directly from `next` because our version can work simultaneously with
16+
// multiple versions of next. See note in `wrappers/types` for more.
17+
import type { NextApiHandler } from '../../server/types';
18+
19+
type NextApiModule =
20+
| {
21+
// ESM export
22+
default?: NextApiHandler; // TODO CHANGE THIS TYPE
23+
middleware?: NextApiHandler; // TODO CHANGE THIS TYPE
24+
}
25+
// CJS export
26+
| NextApiHandler;
27+
28+
const userApiModule = origModule as NextApiModule;
29+
30+
// Default to undefined. It's possible for Next.js users to not define any exports/handlers in an API route. If that is
31+
// the case Next.js wil crash during runtime but the Sentry SDK should definitely not crash so we need tohandle it.
32+
let userProvidedNamedHandler: NextApiHandler | undefined = undefined;
33+
let userProvidedDefaultHandler: NextApiHandler | undefined = undefined;
34+
35+
if ('middleware' in userApiModule && typeof userApiModule.middleware === 'function') {
36+
// Handle when user defines via named ESM export: `export { middleware };`
37+
userProvidedNamedHandler = userApiModule.middleware;
38+
} else if ('default' in userApiModule && typeof userApiModule.default === 'function') {
39+
// Handle when user defines via ESM export: `export default myFunction;`
40+
userProvidedDefaultHandler = userApiModule.default;
41+
} else if (typeof userApiModule === 'function') {
42+
// Handle when user defines via CJS export: "module.exports = myFunction;"
43+
userProvidedDefaultHandler = userApiModule;
44+
}
45+
46+
export const middleware = userProvidedNamedHandler ? Sentry.withSentryMiddleware(userProvidedNamedHandler) : undefined;
47+
export default userProvidedDefaultHandler ? Sentry.withSentryMiddleware(userProvidedDefaultHandler) : undefined;
48+
49+
// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to
50+
// not include anything whose name matchs something we've explicitly exported above.
51+
// @ts-ignore See above
52+
// eslint-disable-next-line import/no-unresolved
53+
export * from '__SENTRY_WRAPPING_TARGET__';

packages/nextjs/src/config/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export type EntryPointObject = { import: string | Array<string> };
164164
*/
165165

166166
export type WebpackModuleRule = {
167-
test?: string | RegExp;
167+
test?: string | RegExp | ((resourcePath: string) => boolean);
168168
include?: Array<string | RegExp> | RegExp;
169169
exclude?: (filepath: string) => boolean;
170170
use?: ModuleRuleUseProperty | Array<ModuleRuleUseProperty>;

packages/nextjs/src/config/webpack.ts

+42-3
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,59 @@ export function constructWebpackConfigFunction(
9999

100100
if (isServer) {
101101
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
102-
const pagesDir = newConfig.resolve?.alias?.['private-next-pages'] as string;
102+
let pagesDirPath: string;
103+
if (
104+
fs.existsSync(path.join(projectDir, 'pages')) &&
105+
fs.lstatSync(path.join(projectDir, 'pages')).isDirectory()
106+
) {
107+
pagesDirPath = path.join(projectDir, 'pages');
108+
} else {
109+
pagesDirPath = path.join(projectDir, 'src', 'pages');
110+
}
111+
112+
const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js');
113+
const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts');
103114

104115
// Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
105116
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
117+
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
106118
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
107119

108120
// It is very important that we insert our loader at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened.
109121
newConfig.module.rules.unshift({
110-
test: new RegExp(`^${escapeStringForRegex(pagesDir)}.*\\.(${pageExtensionRegex})$`),
122+
test: resourcePath => {
123+
// We generally want to apply the loader to all API routes, pages and to the middleware file.
124+
125+
// `resourcePath` may be an absolute path or a path relative to the context of the webpack config
126+
let absoluteResourcePath: string;
127+
if (path.isAbsolute(resourcePath)) {
128+
absoluteResourcePath = resourcePath;
129+
} else {
130+
absoluteResourcePath = path.join(projectDir, resourcePath);
131+
}
132+
const normalizedAbsoluteResourcePath = path.normalize(absoluteResourcePath);
133+
134+
if (
135+
// Match everything inside pages/ with the appropriate file extension
136+
normalizedAbsoluteResourcePath.startsWith(pagesDirPath) &&
137+
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
138+
) {
139+
return true;
140+
} else if (
141+
// Match middleware.js and middleware.ts
142+
normalizedAbsoluteResourcePath === middlewareJsPath ||
143+
normalizedAbsoluteResourcePath === middlewareTsPath
144+
) {
145+
return true;
146+
} else {
147+
return false;
148+
}
149+
},
111150
use: [
112151
{
113152
loader: path.resolve(__dirname, 'loaders/wrappingLoader.js'),
114153
options: {
115-
pagesDir,
154+
pagesDir: pagesDirPath,
116155
pageExtensionRegex,
117156
excludeServerRoutes: userSentryOptions.excludeServerRoutes,
118157
},

0 commit comments

Comments
 (0)