@sentry/nextjs
Version:
Official Sentry SDK for Next.js
253 lines (250 loc) • 12 kB
JavaScript
import commonjs from '@rollup/plugin-commonjs';
import { stringMatchesSomePattern } from '@sentry/core';
import * as fs from 'fs';
import * as path from 'path';
import { rollup } from 'rollup';
const SENTRY_WRAPPER_MODULE_NAME = "sentry-wrapper-module";
const WRAPPING_TARGET_MODULE_NAME = "__SENTRY_WRAPPING_TARGET_FILE__.cjs";
const apiWrapperTemplatePath = path.resolve(__dirname, "..", "templates", "apiWrapperTemplate.js");
const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: "utf8" });
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" });
let showedMissingAsyncStorageModuleWarning = false;
const serverComponentWrapperTemplatePath = path.resolve(
__dirname,
"..",
"templates",
"serverComponentWrapperTemplate.js"
);
const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrapperTemplatePath, { encoding: "utf8" });
const routeHandlerWrapperTemplatePath = path.resolve(__dirname, "..", "templates", "routeHandlerWrapperTemplate.js");
const routeHandlerWrapperTemplateCode = fs.readFileSync(routeHandlerWrapperTemplatePath, { encoding: "utf8" });
function wrappingLoader(userCode, userModuleSourceMap) {
const {
pagesDir,
appDir,
pageExtensionRegex,
excludeServerRoutes = [],
wrappingTargetKind,
vercelCronsConfig,
nextjsRequestAsyncStorageModulePath,
isDev
} = "getOptions" in this ? this.getOptions() : this.query;
this.async();
let templateCode;
if (wrappingTargetKind === "page" || wrappingTargetKind === "api-route") {
if (pagesDir === void 0) {
this.callback(null, userCode, userModuleSourceMap);
return;
}
const parameterizedPagesRoute = path.relative(pagesDir, this.resourcePath).replace(/\\/g, "/").replace(/(.*)/, "/$1").replace(new RegExp(`\\.(${pageExtensionRegex})`), "").replace(/\/index$/, "").replace(/^$/, "/");
if (stringMatchesSomePattern(parameterizedPagesRoute, excludeServerRoutes, true)) {
this.callback(null, userCode, userModuleSourceMap);
return;
}
if (wrappingTargetKind === "page") {
templateCode = pageWrapperTemplateCode;
} else if (wrappingTargetKind === "api-route") {
templateCode = apiWrapperTemplateCode;
} else {
throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`);
}
templateCode = templateCode.replace(/__VERCEL_CRONS_CONFIGURATION__/g, JSON.stringify(vercelCronsConfig));
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, "\\\\"));
} else if (wrappingTargetKind === "server-component" || wrappingTargetKind === "route-handler") {
if (appDir === void 0) {
this.callback(null, userCode, userModuleSourceMap);
return;
}
const parameterizedPagesRoute = path.relative(appDir, this.resourcePath).replace(/\\/g, "/").replace(/(.*)/, "/$1").replace(/\/[^/]+\.(js|ts|jsx|tsx)$/, "").replace(/^$/, "/");
if (stringMatchesSomePattern(parameterizedPagesRoute, excludeServerRoutes, true)) {
this.callback(null, userCode, userModuleSourceMap);
return;
}
if (userCode.includes("__next_internal_client_entry_do_not_use__")) {
this.callback(null, userCode, userModuleSourceMap);
return;
}
if (wrappingTargetKind === "server-component") {
templateCode = serverComponentWrapperTemplateCode;
} else {
templateCode = routeHandlerWrapperTemplateCode;
}
if (nextjsRequestAsyncStorageModulePath !== void 0) {
templateCode = templateCode.replace(
/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
nextjsRequestAsyncStorageModulePath
);
} else {
if (!showedMissingAsyncStorageModuleWarning) {
console.warn(
"[@sentry/nextjs] The Sentry SDK could not access the 'RequestAsyncStorage' module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this."
);
showedMissingAsyncStorageModuleWarning = true;
}
templateCode = templateCode.replace(
/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
"@sentry/nextjs/async-storage-shim"
);
}
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, "\\\\"));
const componentTypeMatch = path.posix.normalize(path.relative(appDir, this.resourcePath)).replace(/\\/g, "/").match(new RegExp(`(?:^|/)?([^/]+)\\.(?:${pageExtensionRegex})$`));
if (componentTypeMatch?.[1]) {
let componentType;
switch (componentTypeMatch[1]) {
case "page":
componentType = "Page";
break;
case "layout":
componentType = "Layout";
break;
case "head":
componentType = "Head";
break;
case "not-found":
componentType = "Not-found";
break;
case "loading":
componentType = "Loading";
break;
default:
componentType = "Unknown";
}
templateCode = templateCode.replace(/__COMPONENT_TYPE__/g, componentType);
} else {
templateCode = templateCode.replace(/__COMPONENT_TYPE__/g, "Unknown");
}
} else if (wrappingTargetKind === "middleware") {
templateCode = middlewareWrapperTemplateCode;
} else {
throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`);
}
templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET_FILE__/g, WRAPPING_TARGET_MODULE_NAME);
wrapUserCode(templateCode, userCode, userModuleSourceMap, isDev, this.resourcePath).then(({ code: wrappedCode, map: wrappedCodeSourceMap }) => {
this.callback(null, wrappedCode, wrappedCodeSourceMap);
}).catch((err) => {
console.warn(
`[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:
${err}`
);
this.callback(null, userCode, userModuleSourceMap);
});
}
async function wrapUserCode(wrapperCode, userModuleCode, userModuleSourceMap, isDev, userModulePath) {
const wrap = (withDefaultExport) => rollup({
input: SENTRY_WRAPPER_MODULE_NAME,
plugins: [
// We're using a simple custom plugin that virtualizes our wrapper module and the user module, so we don't have to
// mess around with file paths and so that we can pass the original user module source map to rollup so that
// rollup gives us a bundle with correct source mapping to the original file
{
name: "virtualize-sentry-wrapper-modules",
resolveId: (id) => {
if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) {
return id;
}
return null;
},
load(id) {
if (id === SENTRY_WRAPPER_MODULE_NAME) {
return withDefaultExport ? wrapperCode : wrapperCode.replace("export { default } from", "export {} from");
}
if (id !== WRAPPING_TARGET_MODULE_NAME) {
return null;
}
if (!isDev || !userModulePath) {
return { code: userModuleCode, map: userModuleSourceMap };
}
const userSources = userModuleSourceMap?.sources;
if (Array.isArray(userSources)) {
return {
code: userModuleCode,
map: {
...userModuleSourceMap,
sources: userSources.map((source, index) => index === 0 ? userModulePath : source)
}
};
}
return {
code: userModuleCode,
map: {
version: 3,
sources: [userModulePath],
sourcesContent: [userModuleCode],
mappings: ""
}
};
}
},
// People may use `module.exports` in their API routes or page files. Next.js allows that and we also need to
// handle that correctly so we let a plugin to take care of bundling cjs exports for us.
commonjs({
sourceMap: true,
strictRequires: true,
// Don't hoist require statements that users may define
ignoreDynamicRequires: true,
// Don't break dynamic requires and things like Webpack's `require.context`
ignore() {
return true;
}
})
],
// We only want to bundle our wrapper module and the wrappee module into one, so we mark everything else as external.
external: (sourceId) => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME,
// Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the
// user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and
// https://stackoverflow.com/a/60347490.)
context: "this",
// Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root
// level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what
// seems to happen is this:
//
// - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'`
// - Rollup converts '../../utils/helper' into an absolute path
// - We mark the helper module as external
// - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is
// the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the
// bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped
// live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the
// root. Unclear why it's not.)
// - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'`
// rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in
// nextjs..
//
// Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of
// externals entirely, with the result that their paths remain untouched (which is what we want).
makeAbsoluteExternalsRelative: false,
onwarn: (_warning, _warn) => {
}
});
let rollupBuild;
try {
rollupBuild = await wrap(true);
} catch (e) {
if (e?.code === "MISSING_EXPORT") {
rollupBuild = await wrap(false);
} else {
throw e;
}
}
const finalBundle = await rollupBuild.generate({
format: "esm",
// In dev mode, use inline sourcemaps so debuggers can map breakpoints back to original source.
// In production, use hidden sourcemaps (no sourceMappingURL comment) to avoid exposing internals.
sourcemap: isDev ? "inline" : "hidden",
// In dev mode, preserve absolute paths in sourcemaps so debuggers can correctly resolve breakpoints.
// By default, Rollup converts absolute paths to relative paths, which breaks debugging.
// We only do this in dev mode to avoid interfering with Sentry's sourcemap upload in production.
sourcemapPathTransform: isDev ? (relativeSourcePath) => {
if (userModulePath?.endsWith(relativeSourcePath)) {
return userModulePath;
}
return relativeSourcePath;
} : void 0
});
return finalBundle.output[0];
}
export { wrappingLoader as default };
//# sourceMappingURL=wrappingLoader.js.map