Skip to content

Commit

Permalink
feat(nextjs): Auto-wrap edge-routes and middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Jan 13, 2023
1 parent 6f4b028 commit 416a7c8
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 13 deletions.
6 changes: 5 additions & 1 deletion packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export default [
),
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/config/templates/pageWrapperTemplate.ts', 'src/config/templates/apiWrapperTemplate.ts'],
entrypoints: [
'src/config/templates/pageWrapperTemplate.ts',
'src/config/templates/apiWrapperTemplate.ts',
'src/config/templates/middlewareWrapperTemplate.ts',
],

packageSpecificConfig: {
output: {
Expand Down
21 changes: 14 additions & 7 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encodin
const pageWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'pageWrapperTemplate.js');
const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encoding: 'utf8' });

const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js');
const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' });

// Just a simple placeholder to make referencing module consistent
const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';

Expand Down Expand Up @@ -40,14 +43,8 @@ export default function wrappingLoader(
pagesDir,
pageExtensionRegex,
excludeServerRoutes = [],
isEdgeRuntime,
} = 'getOptions' in this ? this.getOptions() : this.query;

// We currently don't support the edge runtime
if (isEdgeRuntime) {
return userCode;
}

this.async();

// Get the parameterized route name from this page's filepath
Expand All @@ -71,7 +68,17 @@ export default function wrappingLoader(
return;
}

let templateCode = parameterizedRoute.startsWith('/api') ? apiWrapperTemplateCode : pageWrapperTemplateCode;
const middlewareJsPath = path.join(pagesDir, '..', 'middleware.js');
const middlewareTsPath = path.join(pagesDir, '..', 'middleware.js');

let templateCode: string;
if (parameterizedRoute.startsWith('/api')) {
templateCode = apiWrapperTemplateCode;
} else if (this.resourcePath === middlewareJsPath || this.resourcePath === middlewareTsPath) {
templateCode = middlewareWrapperTemplateCode;
} else {
templateCode = pageWrapperTemplateCode;
}

// Inject the route and the path to the file we're wrapping into the template
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedRoute.replace(/\\/g, '\\\\'));
Expand Down
53 changes: 53 additions & 0 deletions packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* This file is a template for the code which will be substituted when our webpack loader handles API files in the
* `pages/` directory.
*
* We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package,
* this causes both TS and ESLint to complain, hence the pragma comments below.
*/

// @ts-ignore See above
// eslint-disable-next-line import/no-unresolved
import * as origModule from '__SENTRY_WRAPPING_TARGET__';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Sentry from '@sentry/nextjs';

// We import this from `wrappers` rather than directly from `next` because our version can work simultaneously with
// multiple versions of next. See note in `wrappers/types` for more.
import type { NextApiHandler } from '../../server/types';

type NextApiModule =
| {
// ESM export
default?: NextApiHandler; // TODO CHANGE THIS TYPE
middleware?: NextApiHandler; // TODO CHANGE THIS TYPE
}
// CJS export
| NextApiHandler;

const userApiModule = origModule as NextApiModule;

// Default to undefined. It's possible for Next.js users to not define any exports/handlers in an API route. If that is
// the case Next.js wil crash during runtime but the Sentry SDK should definitely not crash so we need tohandle it.
let userProvidedNamedHandler: NextApiHandler | undefined = undefined;
let userProvidedDefaultHandler: NextApiHandler | undefined = undefined;

if ('middleware' in userApiModule && typeof userApiModule.middleware === 'function') {
// Handle when user defines via named ESM export: `export { middleware };`
userProvidedNamedHandler = userApiModule.middleware;
} else if ('default' in userApiModule && typeof userApiModule.default === 'function') {
// Handle when user defines via ESM export: `export default myFunction;`
userProvidedDefaultHandler = userApiModule.default;
} else if (typeof userApiModule === 'function') {
// Handle when user defines via CJS export: "module.exports = myFunction;"
userProvidedDefaultHandler = userApiModule;
}

export const middleware = userProvidedNamedHandler ? Sentry.withSentryMiddleware(userProvidedNamedHandler) : undefined;
export default userProvidedDefaultHandler ? Sentry.withSentryMiddleware(userProvidedDefaultHandler) : undefined;

// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to
// not include anything whose name matchs something we've explicitly exported above.
// @ts-ignore See above
// eslint-disable-next-line import/no-unresolved
export * from '__SENTRY_WRAPPING_TARGET__';
2 changes: 1 addition & 1 deletion packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export type EntryPointObject = { import: string | Array<string> };
*/

export type WebpackModuleRule = {
test?: string | RegExp;
test?: string | RegExp | ((resourcePath: string) => boolean);
include?: Array<string | RegExp> | RegExp;
exclude?: (filepath: string) => boolean;
use?: ModuleRuleUseProperty | Array<ModuleRuleUseProperty>;
Expand Down
46 changes: 42 additions & 4 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,61 @@ export function constructWebpackConfigFunction(

if (isServer) {
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
const pagesDir = newConfig.resolve?.alias?.['private-next-pages'] as string;
let pagesDirPath: string;
if (
fs.existsSync(path.join(projectDir, 'pages')) &&
fs.lstatSync(path.join(projectDir, 'pages')).isDirectory()
) {
pagesDirPath = path.join(projectDir, 'pages');
} else {
pagesDirPath = path.join(projectDir, 'src', 'pages');
}

const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js');
const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts');

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

// 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.
newConfig.module.rules.unshift({
test: new RegExp(`^${escapeStringForRegex(pagesDir)}.*\\.(${pageExtensionRegex})$`),
test: resourcePath => {
// We generally want to apply the loader to all API routes, pages and to the middleware file.

// `resourcePath` may be an absolute path or a path relative to the context of the webpack config
let absoluteResourcePath: string;
if (path.isAbsolute(resourcePath)) {
absoluteResourcePath = resourcePath;
} else {
absoluteResourcePath = path.join(projectDir, resourcePath);
}
const normalizedAbsoluteResourcePath = path.normalize(absoluteResourcePath);

if (
// Match everything inside pages/ with the appropriate file extension
normalizedAbsoluteResourcePath.startsWith(pagesDirPath) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
) {
return true;
} else if (
// Match middleware.js and middleware.ts
normalizedAbsoluteResourcePath === middlewareJsPath ||
normalizedAbsoluteResourcePath === middlewareTsPath
) {
return true;
} else {
return false;
}
},
use: [
{
loader: path.resolve(__dirname, 'loaders/wrappingLoader.js'),
options: {
pagesDir,
pagesDir: pagesDirPath,
pageExtensionRegex,
excludeServerRoutes: userSentryOptions.excludeServerRoutes,
isEdgeRuntime: buildContext.nextRuntime === 'edge',
},
},
],
Expand Down

0 comments on commit 416a7c8

Please sign in to comment.