UNPKG

@sentry/nextjs

Version:
356 lines (315 loc) 16.2 kB
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } }); const commonjs = require('@rollup/plugin-commonjs'); const core = require('@sentry/core'); const chalk = require('chalk'); const fs = require('fs'); const path = require('path'); const rollup = require('rollup'); // Just a simple placeholder to make referencing module consistent const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; // Needs to end in .cjs in order for the `commonjs` plugin to pick it up 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' }); /** * Replace the loaded file with a wrapped version the original file. In the wrapped version, the original file is loaded, * any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) or API routes it contains * are wrapped, and then everything is re-exported. */ // eslint-disable-next-line complexity function wrappingLoader( userCode, // eslint-disable-next-line @typescript-eslint/no-explicit-any userModuleSourceMap, ) { // We know one or the other will be defined, depending on the version of webpack being used const { pagesDir, appDir, pageExtensionRegex, excludeServerRoutes = [], wrappingTargetKind, vercelCronsConfig, nextjsRequestAsyncStorageModulePath, } = 'getOptions' in this ? this.getOptions() : this.query; this.async(); let templateCode; if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') { if (pagesDir === undefined) { this.callback(null, userCode, userModuleSourceMap); return; } // Get the parameterized route name from this page's filepath const parameterizedPagesRoute = path // Get the path of the file inside of the pages directory .relative(pagesDir, this.resourcePath) // Replace all backslashes with forward slashes (windows) .replace(/\\/g, '/') // Add a slash at the beginning .replace(/(.*)/, '/$1') // Pull off the file extension // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input .replace(new RegExp(`\\.(${pageExtensionRegex})`), '') // Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into // just `/xyz` .replace(/\/index$/, '') // In case all of the above have left us with an empty string (which will happen if we're dealing with the // homepage), sub back in the root route .replace(/^$/, '/'); // Skip explicitly-ignored pages if (core.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)); // Inject the route and the path to the file we're wrapping into the template templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\')); } else if (wrappingTargetKind === 'server-component' || wrappingTargetKind === 'route-handler') { if (appDir === undefined) { this.callback(null, userCode, userModuleSourceMap); return; } // Get the parameterized route name from this page's filepath const parameterizedPagesRoute = path // Get the path of the file inside of the app directory .relative(appDir, this.resourcePath) // Replace all backslashes with forward slashes (windows) .replace(/\\/g, '/') // Add a slash at the beginning .replace(/(.*)/, '/$1') // Pull off the file name .replace(/\/[^/]+\.(js|ts|jsx|tsx)$/, '') // In case all of the above have left us with an empty string (which will happen if we're dealing with the // homepage), sub back in the root route .replace(/^$/, '/'); // Skip explicitly-ignored pages if (core.stringMatchesSomePattern(parameterizedPagesRoute, excludeServerRoutes, true)) { this.callback(null, userCode, userModuleSourceMap); return; } // The following string is what Next.js injects in order to mark client components: // https://github.com/vercel/next.js/blob/295f9da393f7d5a49b0c2e15a2f46448dbdc3895/packages/next/build/analysis/get-page-static-info.ts#L37 // https://github.com/vercel/next.js/blob/a1c15d84d906a8adf1667332a3f0732be615afa0/packages/next-swc/crates/core/src/react_server_components.rs#L247 // We do not want to wrap client components 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 !== undefined) { templateCode = templateCode.replace( /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, nextjsRequestAsyncStorageModulePath, ); } else { if (!showedMissingAsyncStorageModuleWarning) { // eslint-disable-next-line no-console console.warn( `${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan( 'RequestAsyncStorage', )} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`, ); 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 all backslashes with forward slashes (windows) .replace(/\\/g, '/') // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor .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}"`); } // Replace the import path of the wrapping target in the template with a path that the `wrapUserCode` function will understand. templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET_FILE__/g, WRAPPING_TARGET_MODULE_NAME); // Run the proxy module code through Rollup, in order to split the `export * from '<wrapped file>'` out into // individual exports (which nextjs seems to require). wrapUserCode(templateCode, userCode, userModuleSourceMap) .then(({ code: wrappedCode, map: wrappedCodeSourceMap }) => { this.callback(null, wrappedCode, wrappedCodeSourceMap); }) .catch(err => { // eslint-disable-next-line no-console console.warn( `[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:\n${err}`, ); this.callback(null, userCode, userModuleSourceMap); }); } /** * Use Rollup to process the proxy module code, in order to split its `export * from '<wrapped file>'` call into * individual exports (which nextjs seems to need). * * Wraps provided user code (located under the import defined via WRAPPING_TARGET_MODULE_NAME) with provided wrapper * code. Under the hood, this function uses rollup to bundle the modules together. Rollup is convenient for us because * it turns `export * from '<wrapped file>'` (which Next.js doesn't allow) into individual named exports. * * Note: This function may throw in case something goes wrong while bundling. * * @param wrapperCode The wrapper module code * @param userModuleCode The user module code * @returns The wrapped user code and a source map that describes the transformations done by this function */ async function wrapUserCode( wrapperCode, userModuleCode, // eslint-disable-next-line @typescript-eslint/no-explicit-any userModuleSourceMap, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { const wrap = (withDefaultExport) => rollup.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; } else { return null; } }, load(id) { if (id === SENTRY_WRAPPER_MODULE_NAME) { return withDefaultExport ? wrapperCode : wrapperCode.replace('export { default } from', 'export {} from'); } else if (id === WRAPPING_TARGET_MODULE_NAME) { return { code: userModuleCode, map: userModuleSourceMap, // give rollup access to original user module source map }; } else { return null; } }, }, // 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.default({ 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() { // We basically only want to use this plugin for handling the case where users export their handlers with module.exports. // 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. // (Also, modifying require may break user code) 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) => { // Suppress all warnings - we don't want to bother people with this output // Might be stuff like "you have unused imports" // _warn(_warning); // uncomment to debug }, }); // Next.js sometimes complains if you define a default export (e.g. in route handlers in dev mode). // This is why we want to avoid unnecessarily creating default exports, even if they're just `undefined`. // For this reason we try to bundle/wrap the user code once including a re-export of `default`. // If the user code didn't have a default export, rollup will throw. // We then try bundling/wrapping again, but without including a re-export of `default`. 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', sourcemap: 'hidden', // put source map data in the bundle but don't generate a source map comment in the output }); // The module at index 0 is always the entrypoint, which in this case is the proxy module. return finalBundle.output[0]; } exports.default = wrappingLoader; //# sourceMappingURL=wrappingLoader.js.map