Skip to content

Commit b57e139

Browse files
lforstLms24
andauthored
fix(nextjs): Don't rexport default in route handlers (#8924)
Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
1 parent 29ef20e commit b57e139

File tree

3 files changed

+103
-92
lines changed

3 files changed

+103
-92
lines changed

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

Lines changed: 93 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { stringMatchesSomePattern } from '@sentry/utils';
33
import * as chalk from 'chalk';
44
import * as fs from 'fs';
55
import * as path from 'path';
6+
import type { RollupBuild, RollupError } from 'rollup';
67
import { rollup } from 'rollup';
78

89
import type { VercelCronsConfig } from '../../common/types';
@@ -277,85 +278,101 @@ async function wrapUserCode(
277278
userModuleCode: string,
278279
userModuleSourceMap: any,
279280
): Promise<{ code: string; map?: any }> {
280-
const rollupBuild = await rollup({
281-
input: SENTRY_WRAPPER_MODULE_NAME,
282-
283-
plugins: [
284-
// We're using a simple custom plugin that virtualizes our wrapper module and the user module, so we don't have to
285-
// mess around with file paths and so that we can pass the original user module source map to rollup so that
286-
// rollup gives us a bundle with correct source mapping to the original file
287-
{
288-
name: 'virtualize-sentry-wrapper-modules',
289-
resolveId: id => {
290-
if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) {
291-
return id;
292-
} else {
293-
return null;
294-
}
295-
},
296-
load(id) {
297-
if (id === SENTRY_WRAPPER_MODULE_NAME) {
298-
return wrapperCode;
299-
} else if (id === WRAPPING_TARGET_MODULE_NAME) {
300-
return {
301-
code: userModuleCode,
302-
map: userModuleSourceMap, // give rollup acces to original user module source map
303-
};
304-
} else {
305-
return null;
306-
}
281+
const wrap = (withDefaultExport: boolean): Promise<RollupBuild> =>
282+
rollup({
283+
input: SENTRY_WRAPPER_MODULE_NAME,
284+
285+
plugins: [
286+
// We're using a simple custom plugin that virtualizes our wrapper module and the user module, so we don't have to
287+
// mess around with file paths and so that we can pass the original user module source map to rollup so that
288+
// rollup gives us a bundle with correct source mapping to the original file
289+
{
290+
name: 'virtualize-sentry-wrapper-modules',
291+
resolveId: id => {
292+
if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) {
293+
return id;
294+
} else {
295+
return null;
296+
}
297+
},
298+
load(id) {
299+
if (id === SENTRY_WRAPPER_MODULE_NAME) {
300+
return withDefaultExport ? wrapperCode : wrapperCode.replace('export { default } from', 'export {} from');
301+
} else if (id === WRAPPING_TARGET_MODULE_NAME) {
302+
return {
303+
code: userModuleCode,
304+
map: userModuleSourceMap, // give rollup acces to original user module source map
305+
};
306+
} else {
307+
return null;
308+
}
309+
},
307310
},
311+
312+
// People may use `module.exports` in their API routes or page files. Next.js allows that and we also need to
313+
// handle that correctly so we let a plugin to take care of bundling cjs exports for us.
314+
commonjs({
315+
sourceMap: true,
316+
strictRequires: true, // Don't hoist require statements that users may define
317+
ignoreDynamicRequires: true, // Don't break dynamic requires and things like Webpack's `require.context`
318+
ignore() {
319+
// We basically only want to use this plugin for handling the case where users export their handlers with module.exports.
320+
// This plugin would also be able to convert any `require` into something esm compatible but webpack does that anyways so we just skip that part of the plugin.
321+
// (Also, modifying require may break user code)
322+
return true;
323+
},
324+
}),
325+
],
326+
327+
// We only want to bundle our wrapper module and the wrappee module into one, so we mark everything else as external.
328+
external: sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME,
329+
330+
// Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the
331+
// user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and
332+
// https://stackoverflow.com/a/60347490.)
333+
context: 'this',
334+
335+
// Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root
336+
// level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what
337+
// seems to happen is this:
338+
//
339+
// - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'`
340+
// - Rollup converts '../../utils/helper' into an absolute path
341+
// - We mark the helper module as external
342+
// - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is
343+
// the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the
344+
// bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped
345+
// live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the
346+
// root. Unclear why it's not.)
347+
// - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'`
348+
// rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in
349+
// nextjs..
350+
//
351+
// Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of
352+
// externals entirely, with the result that their paths remain untouched (which is what we want).
353+
makeAbsoluteExternalsRelative: false,
354+
onwarn: (_warning, _warn) => {
355+
// Suppress all warnings - we don't want to bother people with this output
356+
// Might be stuff like "you have unused imports"
357+
// _warn(_warning); // uncomment to debug
308358
},
359+
});
309360

310-
// People may use `module.exports` in their API routes or page files. Next.js allows that and we also need to
311-
// handle that correctly so we let a plugin to take care of bundling cjs exports for us.
312-
commonjs({
313-
sourceMap: true,
314-
strictRequires: true, // Don't hoist require statements that users may define
315-
ignoreDynamicRequires: true, // Don't break dynamic requires and things like Webpack's `require.context`
316-
ignore() {
317-
// We want basically only want to use this plugin for handling the case where users export their handlers with module.exports.
318-
// This plugin would also be able to convert any `require` into something esm compatible but webpack does that anyways so we just skip that part of the plugin.
319-
// (Also, modifying require may break user code)
320-
return true;
321-
},
322-
}),
323-
],
324-
325-
// We only want to bundle our wrapper module and the wrappee module into one, so we mark everything else as external.
326-
external: sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME,
327-
328-
// Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the
329-
// user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and
330-
// https://stackoverflow.com/a/60347490.)
331-
context: 'this',
332-
333-
// Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root
334-
// level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what
335-
// seems to happen is this:
336-
//
337-
// - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'`
338-
// - Rollup converts '../../utils/helper' into an absolute path
339-
// - We mark the helper module as external
340-
// - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is
341-
// the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the
342-
// bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped
343-
// live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the
344-
// root. Unclear why it's not.)
345-
// - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'`
346-
// rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in
347-
// nextjs..
348-
//
349-
// Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of
350-
// externals entirely, with the result that their paths remain untouched (which is what we want).
351-
makeAbsoluteExternalsRelative: false,
352-
353-
onwarn: (_warning, _warn) => {
354-
// Suppress all warnings - we don't want to bother people with this output
355-
// Might be stuff like "you have unused imports"
356-
// _warn(_warning); // uncomment to debug
357-
},
358-
});
361+
// Next.js sometimes complains if you define a default export (e.g. in route handlers in dev mode).
362+
// This is why we want to avoid unnecessarily creating default exports, even if they're just `undefined`.
363+
// For this reason we try to bundle/wrap the user code once including a re-export of `default`.
364+
// If the user code didn't have a default export, rollup will throw.
365+
// We then try bundling/wrapping agian, but without including a re-export of `default`.
366+
let rollupBuild;
367+
try {
368+
rollupBuild = await wrap(true);
369+
} catch (e) {
370+
if ((e as RollupError)?.code === 'MISSING_EXPORT') {
371+
rollupBuild = await wrap(false);
372+
} else {
373+
throw e;
374+
}
375+
}
359376

360377
const finalBundle = await rollupBuild.generate({
361378
format: 'esm',

packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type { RequestAsyncStorage } from './requestAsyncStorageShim';
1313
declare const requestAsyncStorage: RequestAsyncStorage;
1414

1515
declare const routeModule: {
16-
default: unknown;
1716
GET?: (...args: unknown[]) => unknown;
1817
POST?: (...args: unknown[]) => unknown;
1918
PUT?: (...args: unknown[]) => unknown;
@@ -59,17 +58,18 @@ function wrapHandler<T>(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | '
5958
});
6059
}
6160

61+
// @ts-ignore See above
62+
// eslint-disable-next-line import/no-unresolved
63+
export * from '__SENTRY_WRAPPING_TARGET_FILE__';
64+
65+
// @ts-ignore This is the file we're wrapping
66+
// eslint-disable-next-line import/no-unresolved
67+
export { default } from '__SENTRY_WRAPPING_TARGET_FILE__';
68+
6269
export const GET = wrapHandler(routeModule.GET, 'GET');
6370
export const POST = wrapHandler(routeModule.POST, 'POST');
6471
export const PUT = wrapHandler(routeModule.PUT, 'PUT');
6572
export const PATCH = wrapHandler(routeModule.PATCH, 'PATCH');
6673
export const DELETE = wrapHandler(routeModule.DELETE, 'DELETE');
6774
export const HEAD = wrapHandler(routeModule.HEAD, 'HEAD');
6875
export const OPTIONS = wrapHandler(routeModule.OPTIONS, 'OPTIONS');
69-
70-
// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to
71-
// not include anything whose name matchs something we've explicitly exported above.
72-
// @ts-ignore See above
73-
// eslint-disable-next-line import/no-unresolved
74-
export * from '__SENTRY_WRAPPING_TARGET_FILE__';
75-
export default routeModule.default;

packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@ import '__SENTRY_CONFIG_IMPORT_PATH__';
44

55
// @ts-ignore This is the file we're wrapping
66
// eslint-disable-next-line import/no-unresolved
7-
import * as wrappee from '__SENTRY_WRAPPING_TARGET_FILE__';
8-
9-
// @ts-ignore default either exists, or it doesn't - we don't care
10-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
11-
const defaultExport = wrappee.default;
7+
export * from '__SENTRY_WRAPPING_TARGET_FILE__';
128

139
// @ts-ignore This is the file we're wrapping
1410
// eslint-disable-next-line import/no-unresolved
15-
export * from '__SENTRY_WRAPPING_TARGET_FILE__';
16-
17-
export default defaultExport;
11+
export { default } from '__SENTRY_WRAPPING_TARGET_FILE__';

0 commit comments

Comments
 (0)